Commit 33b1dc13 authored by Amos Wenger's avatar Amos Wenger

Gate windows-specific APIs

parent 962a393a
Pipeline #16074 failed with stage
in 3 minutes and 41 seconds
......@@ -8,4 +8,5 @@
/c-for-go
/husk-sample
/husk-sample.exe
cforgo-defines.h
*.pdb
......@@ -20,3 +20,8 @@ typedef signed long int int32_t;
include_guard = "husk"
includes = []
no_includes = true
[defines]
"target_os = windows" = "TARGET_OS_WINDOWS"
"target_os = linux" = "TARGET_OS_LINUX"
"target_os = macos" = "TARGET_OS_MACOS"
......@@ -6,7 +6,7 @@ GENERATOR:
Includes: ["husk.h"]
PARSER:
SourcesPaths: ["include/husk.h"]
SourcesPaths: ["cforgo-defines.h", "include/husk.h"]
Defines:
C_FOR_GO_WORKAROUNDS: 1
......@@ -19,6 +19,7 @@ TRANSLATOR:
- {action: accept, from: "^XString"}
- {action: accept, from: "^shell_"}
- {action: accept, from: "^xstring_"}
- {action: accept, from: "^husk_"}
- {transform: export}
post-global:
- {load: snakecase}
......
package husk
import (
"fmt"
"reflect"
"unsafe"
import "github.com/itchio/husk/lowhusk"
"github.com/itchio/husk/lowhusk"
)
func AsString(xs *lowhusk.XString) string {
// this returns `*const u8`, ie. `*byte` in cgo, we need 2 casts
xdata := uintptr(unsafe.Pointer(lowhusk.XstringData(xs)))
// this returns `usize`, ie. `uintptr_t` in cgo, we need 1 cast
xlen := int(lowhusk.XstringLen(xs))
// this builds a slice that refers to the data in `xs`
// n.b: "Cap" is irrelevant, we never mutate it.
sh := &reflect.SliceHeader{
Data: xdata,
Len: xlen,
Cap: xlen,
}
// this slice will become invalid as soon as `XstringFree` is called
slice := *(*[]byte)(unsafe.Pointer(sh))
// this copies out from the slice (not obvious)
s := string(slice)
// ...so now `xs` can be freed
lowhusk.XstringFree(xs)
return s
}
func AsError(err *lowhusk.XString) error {
return &HuskError{AsString(err)}
}
type HuskError struct {
Message string
}
func (he *HuskError) Error() string {
return fmt.Sprintf("husk error: %s", he.Message)
}
type ShellLink struct {
inner *lowhusk.ShellLink
}
func NewShellLink() (*ShellLink, error) {
var err *lowhusk.XString
var link *lowhusk.ShellLink
if lowhusk.ShellLinkNew(&link, &err) != 0 {
return nil, AsError(err)
}
return &ShellLink{link}, nil
}
func (l *ShellLink) Load(path string) error {
var pathBytes = []byte(path)
var err *lowhusk.XString
if lowhusk.ShellLinkLoad(l.inner, &pathBytes[0], uint64(len(pathBytes)), &err) != 0 {
return AsError(err)
}
return nil
}
func (l *ShellLink) GetPath() (string, error) {
var path *lowhusk.XString
var err *lowhusk.XString
if lowhusk.ShellLinkGetPath(l.inner, &path, &err) != 0 {
return "", AsError(err)
}
return AsString(path), nil
}
func (l *ShellLink) Free() {
lowhusk.ShellLinkFree(l.inner)
func Hello() {
lowhusk.HuskHello()
}
//+build !windows
package husk
// muffin
//+build windows
package husk
import (
"fmt"
"reflect"
"unsafe"
"github.com/itchio/husk/lowhusk"
)
func AsString(xs *lowhusk.XString) string {
// this returns `*const u8`, ie. `*byte` in cgo, we need 2 casts
xdata := uintptr(unsafe.Pointer(lowhusk.XstringData(xs)))
// this returns `usize`, ie. `uintptr_t` in cgo, we need 1 cast
xlen := int(lowhusk.XstringLen(xs))
// this builds a slice that refers to the data in `xs`
// n.b: "Cap" is irrelevant, we never mutate it.
sh := &reflect.SliceHeader{
Data: xdata,
Len: xlen,
Cap: xlen,
}
// this slice will become invalid as soon as `XstringFree` is called
slice := *(*[]byte)(unsafe.Pointer(sh))
// this copies out from the slice (not obvious)
s := string(slice)
// ...so now `xs` can be freed
lowhusk.XstringFree(xs)
return s
}
func AsError(err *lowhusk.XString) error {
return &HuskError{AsString(err)}
}
type HuskError struct {
Message string
}
func (he *HuskError) Error() string {
return fmt.Sprintf("husk error: %s", he.Message)
}
type ShellLink struct {
inner *lowhusk.ShellLink
}
func NewShellLink() (*ShellLink, error) {
var err *lowhusk.XString
var link *lowhusk.ShellLink
if lowhusk.ShellLinkNew(&link, &err) != 0 {
return nil, AsError(err)
}
return &ShellLink{link}, nil
}
func (l *ShellLink) Load(path string) error {
var pathBytes = []byte(path)
var err *lowhusk.XString
if lowhusk.ShellLinkLoad(l.inner, &pathBytes[0], uint64(len(pathBytes)), &err) != 0 {
return AsError(err)
}
return nil
}
func (l *ShellLink) GetPath() (string, error) {
var path *lowhusk.XString
var err *lowhusk.XString
if lowhusk.ShellLinkGetPath(l.inner, &path, &err) != 0 {
return "", AsError(err)
}
return AsString(path), nil
}
func (l *ShellLink) Free() {
lowhusk.ShellLinkFree(l.inner)
}
......@@ -2,21 +2,10 @@ package main
import (
"fmt"
"log"
"github.com/itchio/husk/husk"
)
func main() {
link, err := husk.NewShellLink()
must(err)
must(link.Load("C:\\Users\\amos\\Desktop\\υπολογιστή-does-not-exist.lnk"))
path, err := link.GetPath()
must(err)
log.Printf("path = %s", path)
sample()
}
func must(err error) {
......
......@@ -173,6 +173,20 @@ function main(args) {
info(`Generating cgo bindings from C headers`);
rmdirSync("lowhusk", { recursive: true });
{
let targetOS = "LINUX";
if (opts.os === "windows") {
targetOS = "WINDOWS";
} else if (opts.os === "darwin") {
targetOS = "MACOS";
}
let defines = `
#define TARGET_OS_${targetOS} 1
`;
info(`In c-for-go defines, using target os ${chalk.green(targetOS)}`);
rmdirSync("cforgo-defines.h", { recursive: true });
writeFileSync("cforgo-defines.h", defines, { encoding: "utf-8" });
}
$(`./c-for-go husk.yml`);
info(`Generating artifacts`);
......@@ -214,20 +228,21 @@ function main(args) {
}
{
let ldflags = `-L@prefix@/lib -lhusk`;
let libs = ["husk"];
if (opts.os === "windows") {
let libs = "ws2_32 advapi32 ole32 shell32 userenv".split(" ");
let libArgs = libs.map((x) => `-l${x}`);
ldflags += ` ${libArgs.join(" ")}`;
libs = [...libs, "ws2_32", "advapi32", "ole32", "shell32", "userenv"];
} else if (opts.os === "linux") {
libs = [...libs, "dl"];
}
let ldflags = `-L@prefix@/lib ${libs.map((x) => `-l${x}`).join(" ")}`;
writeFileSync(`${prefix}/ldflags.txt`, ldflags, writeOpts);
}
}
info(`Building & running sample binary`);
let cflags = readFileSync("./artifacts/cflags.txt", { encoding: "utf8" });
let cflags = readFileSync("./artifacts/cflags.txt", { encoding: "utf-8" });
setenv("CGO_CFLAGS", cflags.replace("@prefix@", prefix));
let ldflags = readFileSync("./artifacts/ldflags.txt", { encoding: "utf8" });
let ldflags = readFileSync("./artifacts/ldflags.txt", { encoding: "utf-8" });
setenv("CGO_LDFLAGS", ldflags.replace("@prefix@", prefix));
$(`go build -o husk-sample`);
......
//+build !windows
package main
import "github.com/itchio/husk/husk"
func sample() {
husk.Hello()
}
//+build windows
package main
import (
"log"
"github.com/itchio/husk/husk"
)
func sample() {
husk.Hello()
link, err := husk.NewShellLink()
must(err)
must(link.Load("C:\Windows\WinSxS\amd64_microsoft-windows-wordpad_31bf3856ad364e35_10.0.18362.267_none_837ed916fe8dbd2f\\Wordpad.lnk"))
path, err := link.GetPath()
must(err)
log.Printf("path = %s", path)
}
mod interfaces;
use interfaces::*;
#[cfg(target_os = "windows")]
mod windows;
use com::{
runtime::{create_instance, init_runtime},
sys::{FAILED, HRESULT},
};
use widestring::U16CString;
// unfortunately, paths/descriptions/etc. of ShellLinks are all
// constrained to `MAX_PATH`.
const MAX_PATH: usize = 260;
struct SimpleError(String);
impl SimpleError {
pub unsafe fn ret(self, p: *mut *mut XString) {
let xs = XString { data: self.0 };
*p = Box::into_raw(Box::new(xs));
}
}
impl<T> From<T> for SimpleError
where
T: std::error::Error,
{
fn from(e: T) -> Self {
Self(format!("{}", e))
}
}
trait FromUtf16 {
fn from_utf16(self, method: &str) -> Result<String, SimpleError>;
}
impl FromUtf16 for Vec<u16> {
fn from_utf16(self, method: &str) -> Result<String, SimpleError> {
let s = U16CString::from_vec_with_nul(self)
.map_err(|_| SimpleError(format!("missing null terminator in {} result", method)))?;
Ok(s.to_string()
.map_err(|_| SimpleError(format!("invalid utf-16 data in {} result", method)))?)
}
}
trait Checked
where
Self: Sized + Copy,
{
fn check(self, method: &str) -> Result<(), SimpleError>;
fn format(self, method: &str) -> SimpleError;
}
impl Checked for HRESULT {
fn check(self, method: &str) -> Result<(), SimpleError> {
if FAILED(self) {
Err(self.format(method))
} else {
Ok(())
}
}
fn format(self, method: &str) -> SimpleError {
SimpleError(format!(
"{} returned system error {} (0x{:x})",
method, self, self
))
}
}
#[repr(i32)]
pub enum ReturnCode {
Ok = 0,
Error = 1,
}
/// Handle for an IShellLink instance
pub struct ShellLink {
instance: com::ComRc<dyn IShellLinkW>,
}
/// Exchange string: passes Rust strings to C
pub struct XString {
data: String,
}
impl From<String> for XString {
fn from(data: String) -> Self {
Self { data }
}
}
impl ShellLink {
fn new() -> Result<Self, SimpleError> {
init_runtime().map_err(|hr| hr.check("init_runtime").unwrap_err())?;
let instance = create_instance::<dyn IShellLinkW>(&CLSID_SHELL_LINK)
.map_err(|hr| hr.check("create_instance<IShellLinkW>").unwrap_err())?;
Ok(Self { instance })
}
fn load(&self, path: &str) -> Result<(), SimpleError> {
let pf = self.instance.get_interface::<dyn IPersistFile>().unwrap();
let path = U16CString::from_str(path)?;
unsafe { pf.load(path.as_ptr(), STGM_READ) }.check("IPersistFile::Load")?;
Ok(())
}
fn get_path(&self) -> Result<String, SimpleError> {
let mut v = vec![0u16; MAX_PATH + 1];
unsafe {
self.instance.get_path(
v.as_mut_ptr(),
v.len() as _,
std::ptr::null_mut(),
SLGP_RAWPATH,
)
}
.check("IShellLinkW::GetPath")?;
Ok(v.from_utf16("IShellLinkW::GetPath")?)
}
}
macro_rules! checked {
($x: expr, $p_err: ident) => {
match $x {
Err(e) => {
SimpleError::from(e).ret($p_err);
return ReturnCode::Error;
}
Ok(x) => x,
}
};
}
#[no_mangle]
pub unsafe extern "C" fn shell_link_new(
p_link: *mut *mut ShellLink,
p_err: *mut *mut XString,
) -> ReturnCode {
let sl = checked!(ShellLink::new(), p_err);
*p_link = Box::into_raw(Box::new(sl));
ReturnCode::Ok
}
#[no_mangle]
pub unsafe extern "C" fn shell_link_load(
link: *mut ShellLink,
path_data: *mut u8,
path_len: usize,
p_err: *mut *mut XString,
) -> ReturnCode {
let v = std::slice::from_raw_parts(path_data, path_len);
let path = checked!(std::str::from_utf8(v), p_err);
checked!((*link).load(path), p_err);
ReturnCode::Ok
}
#[no_mangle]
pub unsafe extern "C" fn shell_link_get_path(
link: *mut ShellLink,
path: *mut *mut XString,
p_err: *mut *mut XString,
) -> ReturnCode {
let res = checked!((*link).get_path(), p_err);
*path = Box::into_raw(Box::new(XString::from(res)));
ReturnCode::Ok
}
#[no_mangle]
pub unsafe extern "C" fn shell_link_free(link: *mut ShellLink) {
drop(Box::from_raw(link))
}
#[cfg(target_os = "windows")]
pub use windows::*;
#[no_mangle]
pub unsafe extern "C" fn xstring_free(xs: *mut XString) {
drop(Box::from_raw(xs))
}
pub unsafe extern "C" fn husk_hello() {
#[cfg(target_os = "windows")]
println!("Hello from husk on Windows!");
#[no_mangle]
pub unsafe extern "C" fn xstring_data(xs: *mut XString) -> *const u8 {
(*xs).data.as_ptr()
}
#[cfg(target_os = "macos")]
println!("Hello from husk on macOS!");
#[no_mangle]
pub unsafe extern "C" fn xstring_len(xs: *mut XString) -> usize {
(*xs).data.len()
#[cfg(target_os = "linux")]
println!("Hello from husk on Linux!");
}
// #[allow(dead_code)]
// fn demo(sl: ShellLink) {
// println!("Press enter to continue...");
// use std::io::BufRead;
// let mut s = String::new();
// std::io::stdin().lock().read_line(&mut s).unwrap();
// init_runtime().unwrap();
// println!("Initialized runtime");
// let i = create_instance::<dyn IShellLinkW>(&CLSID_SHELL_LINK).unwrap();
// println!("Got a ShellLink!");
// let pf = i.get_interface::<dyn IPersistFile>().unwrap();
// println!("Got as a PersistFile!");
// let path = r#"C:\Users\amos\Desktop\WizTree.lnk"#;
// let meta = std::fs::metadata(&path).unwrap();
// println!("meta = {:?}", meta);
// let u16s = U16CString::from_str(path).unwrap();
// unsafe {
// let res = pf.load(u16s.as_ptr(), STGM_READ);
// assert_eq!(res, S_OK);
// }
// println!("called load!");
// let mut v = vec![0u16; 256];
// unsafe {
// let res = i.get_path(v.as_mut_ptr(), v.len() as _, ptr::null_mut(), SLGP_RAWPATH);
// println!("called get_path, checking now...");
// // println!("v = {:?}", v);
// println!("res = {:?}", res);
// }
// let s = U16CString::from(u16s.clone());
// println!("s = {:?}", s.to_string_lossy());
// unsafe {
// let res = i.get_description(v.as_mut_ptr(), v.len() as c_int);
// println!("called get_description, checking now...");
// // println!("v = {:?}", v);
// println!("res = {:?}", res);
// }
// let s = U16CString::from(u16s.clone());
// println!("s = {:?}", s.to_string_lossy());
// }
mod interfaces;
use interfaces::*;
use com::{
runtime::{create_instance, init_runtime},
sys::{FAILED, HRESULT},
};
use widestring::U16CString;
// unfortunately, paths/descriptions/etc. of ShellLinks are all
// constrained to `MAX_PATH`.
const MAX_PATH: usize = 260;
struct SimpleError(String);
impl SimpleError {
pub unsafe fn ret(self, p: *mut *mut XString) {
let xs = XString { data: self.0 };
*p = Box::into_raw(Box::new(xs));
}
}
impl<T> From<T> for SimpleError
where
T: std::error::Error,
{
fn from(e: T) -> Self {
Self(format!("{}", e))
}
}
trait FromUtf16 {
fn from_utf16(self, method: &str) -> Result<String, SimpleError>;
}
impl FromUtf16 for Vec<u16> {
fn from_utf16(self, method: &str) -> Result<String, SimpleError> {
let s = U16CString::from_vec_with_nul(self)
.map_err(|_| SimpleError(format!("missing null terminator in {} result", method)))?;
Ok(s.to_string()
.map_err(|_| SimpleError(format!("invalid utf-16 data in {} result", method)))?)
}
}
trait Checked
where
Self: Sized + Copy,
{
fn check(self, method: &str) -> Result<(), SimpleError>;
fn format(self, method: &str) -> SimpleError;
}
impl Checked for HRESULT {
fn check(self, method: &str) -> Result<(), SimpleError> {
if FAILED(self) {
Err(self.format(method))
} else {
Ok(())
}
}
fn format(self, method: &str) -> SimpleError {
SimpleError(format!(
"{} returned system error {} (0x{:x})",
method, self, self
))
}
}
#[repr(i32)]
pub enum ReturnCode {
Ok = 0,
Error = 1,
}
/// Handle for an IShellLink instance
pub struct ShellLink {
instance: com::ComRc<dyn IShellLinkW>,
}
/// Exchange string: passes Rust strings to C
pub struct XString {
data: String,
}
impl From<String> for XString {
fn from(data: String) -> Self {
Self { data }
}
}
impl ShellLink {
fn new() -> Result<Self, SimpleError> {
init_runtime().map_err(|hr| hr.check("init_runtime").unwrap_err())?;
let instance = create_instance::<dyn IShellLinkW&