diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9d86339..50ed5d6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,6 +34,6 @@ jobs: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose --all -# There are currently no tests for macos - #- name: Run tests - # run: cargo test --verbose -- --skip test_vk_instance_layer_properties \ No newline at end of file + - name: Run tests + # Skipping vulkan test because drivers are not guarenteed to be on the system. + run: cargo test --verbose -- --skip test_vk_instance_layer_properties \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6aa077a..1ccd6fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dylink" -version = "0.5.0" +version = "0.5.1" authors = ["Jonathan Thomason"] edition = "2021" license = "MIT OR Apache-2.0" @@ -12,9 +12,6 @@ publish = true [workspace] members = ["dylink_macro"] -[dependencies] -once_cell = "1.16" - [dependencies.dylink_macro] version = "0.6" path="./dylink_macro" diff --git a/README.md b/README.md index f55ac45..108af29 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Dylink -![Crates.io](https://img.shields.io/crates/l/dylink) ![Crates.io](https://img.shields.io/crates/v/dylink) ![Crates.io](https://img.shields.io/crates/d/dylink) ![docs.rs](https://img.shields.io/docsrs/dylink) ![unsafe:yes](https://img.shields.io/badge/unsafe-yes-red) +![Crates.io](https://img.shields.io/crates/l/dylink) ![Crates.io](https://img.shields.io/crates/v/dylink) ![Crates.io](https://img.shields.io/crates/d/dylink) ![docs.rs](https://img.shields.io/docsrs/dylink) [![dylink-rs](https://github.com/Razordor/dylink/actions/workflows/rust.yml/badge.svg)](https://github.com/Razordor/dylink/actions/workflows/rust.yml) ![unsafe:yes](https://img.shields.io/badge/unsafe-yes-red) -Dylink provides a run-time dynamic linking framework for lazily evaluating shared libraries such as `.dll` files for windows -and `.so` files for unix. When functions are loaded they are evaluated through a thunk for first time calls, which loads the -function from it's respective library. Proceeding calls after initialization have no overhead or additional branching checks, +Dylink provides a run-time dynamic linking framework for lazily evaluating shared libraries such as `.dll` files. +When functions are loaded they are evaluated through a thunk for first time calls, which loads the function from +it's respective library. Proceeding calls after initialization have no overhead or additional branching checks, as the thunk is replaced by the loaded function. ---- @@ -16,11 +16,11 @@ Related links: ## Supported platforms -Dylink has been implemented for all major platforms aside from WASM, but has only been locally tested on Windows and Linux. +Dylink has been implemented for all major platforms. -| Win64 | Linux | MacOS | Unix(other) | WASM | -|:-----:|:-----:|:--------:|:-----------:|------| -| YES | YES | Untested | Untested | NO | +| Windows | Linux | MacOS | WASM | +|:-------:|:-----:|:-----:|------| +| YES | YES | YES | NO | ## Usage @@ -53,6 +53,35 @@ fn main() { } ``` +## Adv. Example + +The `dylink` macro is also sophisticated enough to survive in `impl` blocks, and take advantage of the receiver argument `self`. +Unfortunately, `Self` cannot be internally inferred, so `self` without an explicit type also cannot be inferred. +The example below demonstrates how to work around these issues despite that: + +```rust +use dylink::dylink; + +#[derive(Debug, PartialEq)] +#[repr(transparent)] +struct Foo(u32); + +impl Foo { + #[dylink(name = "Kernel32.dll")] + extern "stdcall" fn GetLastError() -> Foo; + #[dylink(name = "Kernel32.dll")] + extern "stdcall" fn SetLastError(self: Foo); +} + +fn main() { + let foo = Foo(43); + unsafe { + foo.SetLastError(); + assert_eq!(Foo(43), Foo::GetLastError()); + } +} +``` + ### License Licensed under either of diff --git a/dylink_macro/Cargo.toml b/dylink_macro/Cargo.toml index 51c70cf..7e16f6f 100644 --- a/dylink_macro/Cargo.toml +++ b/dylink_macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dylink_macro" -version = "0.6.0" +version = "0.6.1" authors = ["Jonathan Thomason"] edition = "2021" readme = "README.md" diff --git a/dylink_macro/src/attr_data.rs b/dylink_macro/src/attr_data.rs index d1939df..7b85813 100644 --- a/dylink_macro/src/attr_data.rs +++ b/dylink_macro/src/attr_data.rs @@ -1,12 +1,13 @@ // Copyright (c) 2023 Jonathan "Razordor" Alan Thomason -use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; +use proc_macro2::TokenStream as TokenStream2; use std::str::FromStr; use syn::punctuated::Punctuated; use syn::{spanned::Spanned, *}; pub struct AttrData { - pub strip: Option, + pub strip: bool, pub link_ty: LinkType, + pub linker: Option, } #[derive(PartialEq)] @@ -19,8 +20,9 @@ pub enum LinkType { impl TryFrom> for AttrData { type Error = syn::Error; fn try_from(value: Punctuated) -> Result { - let mut strip: Option = None; + let mut maybe_strip: Option = None; let mut maybe_link_ty: Option = None; + let mut linker: Option = None; let mut errors = vec![]; const EXPECTED_KW: &str = "Expected `vulkan`, `any`, `strip`, or `name`."; @@ -38,11 +40,14 @@ impl TryFrom> for AttrData { errors.push(Error::new(path.span(), EXPECTED_KW)); } } - // Branch for syntax: #[dylink(name = "")] Expr::Assign(assign) => { let (assign_left, assign_right) = (assign.left.as_ref(), assign.right.as_ref()); - if matches!(assign_left, Expr::Path(ExprPath { path, .. }) if path.is_ident("name")) - { + + let Expr::Path(ExprPath { path, .. }) = assign_left else { + unreachable!("internal error when parsing Expr::Assign"); + }; + if path.is_ident("name") { + // Branch for syntax: #[dylink(name = )] match assign_right { Expr::Lit(ExprLit { lit: Lit::Str(lib), .. @@ -60,28 +65,43 @@ impl TryFrom> for AttrData { errors.push(Error::new(right.span(), "Expected string literal.")) } } - } else if matches!(assign_left, Expr::Path(ExprPath { path, .. }) if path.is_ident("strip")) - { + } else if path.is_ident("strip") { + // Branch for syntax: #[dylink(strip = )] match assign_right { Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }) => { - if val.value() { - if strip.is_none() { - strip = Some(val.span()) - } else { - errors.push(Error::new( - assign.span(), - "strip is already defined", - )); - } + if maybe_strip.is_none() { + maybe_strip = Some(val.value()); + } else { + errors.push(Error::new( + assign.span(), + "strip is already defined", + )); } } right => { errors.push(Error::new(right.span(), "Expected boolean literal.")) } } + } else if path.is_ident("linker") { + // Branch for syntax: #[dylink(linker = )] + match assign_right { + Expr::Path(ExprPath { path, .. }) => { + if linker.is_none() { + linker = Some(path.get_ident().unwrap().clone()); + } else { + errors.push(Error::new( + assign.span(), + "linker is already defined", + )); + } + } + right => { + errors.push(Error::new(right.span(), "Expected identifier.")) + } + } } else { errors.push(Error::new(assign_left.span(), EXPECTED_KW)); } @@ -152,8 +172,9 @@ impl TryFrom> for AttrData { } } else { Ok(Self { - strip, + strip: maybe_strip.unwrap_or_default(), link_ty: maybe_link_ty.unwrap(), + linker, }) } } diff --git a/dylink_macro/src/lib.rs b/dylink_macro/src/lib.rs index 144bce8..19317b4 100644 --- a/dylink_macro/src/lib.rs +++ b/dylink_macro/src/lib.rs @@ -9,44 +9,48 @@ use quote::*; use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned, Expr, Token}; use attr_data::*; +use syn::ForeignItem; #[proc_macro_attribute] pub fn dylink(args: TokenStream1, input: TokenStream1) -> TokenStream1 { - let args = TokenStream2::from(args); - let input = TokenStream2::from(input); - let foreign_mod = syn::parse2::(input).expect("failed to parse"); - let punct = Parser::parse2( Punctuated::::parse_separated_nonempty, - args, + args.into(), ) .expect("failed to parse"); - let attr_data = match AttrData::try_from(punct) { - Ok(attr) => attr, - Err(e) => { - return syn::Error::into_compile_error(e).into(); - } - }; - let mut ret = TokenStream2::new(); - for item in foreign_mod.items { - use syn::ForeignItem; - let abi = &foreign_mod.abi; - match item { - ForeignItem::Fn(fn_item) => ret.extend(parse_fn(abi, fn_item, &attr_data)), - other => ret.extend(quote!(#abi {#other})), + match AttrData::try_from(punct) { + Ok(attr_data) => { + if let Ok(foreign_mod) = syn::parse2::(input.clone().into()) { + let abi = &foreign_mod.abi; + foreign_mod + .items + .iter() + .map(|item| match item { + ForeignItem::Fn(fn_item) => parse_fn::(fn_item, &attr_data), + other => quote!(#abi {#other}), + }) + .collect::() + .into() + } else if let Ok(foreign_fn) = syn::parse2::(input.into()) { + parse_fn::(&foreign_fn, &attr_data) + .into() + } else { + panic!("failed to parse"); + } } + Err(e) => syn::Error::into_compile_error(e).into(), } - TokenStream1::from(ret) } -fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) -> TokenStream2 { +fn parse_fn(fn_item: &syn::ForeignItemFn, attr_data: &AttrData) -> TokenStream2 { let fn_name = fn_item.sig.ident.to_token_stream(); - let abi = abi.into_token_stream(); + let abi = fn_item.sig.abi.to_token_stream(); let vis = fn_item.vis.to_token_stream(); let output = fn_item.sig.output.to_token_stream(); let strip = attr_data.strip; let link_ty = &attr_data.link_ty; + let linker = &attr_data.linker; let fn_attrs: Vec = fn_item .attrs @@ -56,6 +60,8 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - let mut param_list = Vec::new(); let mut param_ty_list = Vec::new(); + let mut internal_param_ty_list = Vec::new(); + let mut internal_param_list = Vec::new(); let params_default = fn_item.sig.inputs.to_token_stream(); for (i, arg) in fn_item.sig.inputs.iter().enumerate() { match arg { @@ -67,17 +73,29 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - _ => unreachable!(), }; param_list.push(param_name.clone()); + internal_param_list.push(param_name.clone()); param_ty_list.push(quote!(#param_name : #ty)); + internal_param_ty_list.push(quote!(#param_name : #ty)); } syn::FnArg::Receiver(rec) => { - return syn::Error::new(rec.span(), "`self` arguments are unsupported") - .into_compile_error(); + if IS_MOD_ITEM || strip { + // TODO: fix error message + return syn::Error::new(rec.span(), "`self` arguments are unsupported in this context") + .into_compile_error(); + } else { + let ty = rec.ty.to_token_stream(); + let param_name = format!("p{i}").parse::().unwrap(); + param_list.push(quote!{self}); + internal_param_list.push(param_name.clone()); + param_ty_list.push(quote!(self : #ty)); + internal_param_ty_list.push(quote!(#param_name : #ty)); + } } } } // this is sure to obfuscate things, but this is needed here because `strip` screws with call context. - let caller_name = if strip.is_some() { + let caller_name = if strip { quote! {function} } else { quote! {DYN_FUNC} @@ -88,7 +106,7 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - let call_dyn_func = if *link_ty == LinkType::Vulkan { match fn_name.to_string().as_str() { "vkCreateInstance" => { - if strip.is_some() { + if strip { return syn::Error::new( fn_item.span(), "`vkCreateInstance` is incompatible with `strip=true`", @@ -107,7 +125,7 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - } } "vkDestroyInstance" => { - if strip.is_some() { + if strip { return syn::Error::new( fn_item.span(), "`vkDestroyInstance` is incompatible with `strip=true`", @@ -124,7 +142,7 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - } } "vkCreateDevice" => { - if strip.is_some() { + if strip { return syn::Error::new( fn_item.span(), "`vkCreateDevice` is incompatible with `strip=true`", @@ -141,7 +159,7 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - } } "vkDestroyDevice" => { - if strip.is_some() { + if strip { return syn::Error::new( fn_item.span(), "`vkDestroyDevice` is incompatible with `strip=true`", @@ -163,9 +181,18 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - quote!(#caller_name(#(#param_list),*)) }; + let try_link_call = match linker { + None => quote!{ + try_link + }, + Some(linker_name) => quote!{ + try_link_with::<#linker_name> + }, + }; + // According to "The Rustonomicon" foreign functions are assumed unsafe, // so functions are implicitly prepended with `unsafe` - if strip.is_some() { + if strip { quote! { #(#fn_attrs)* #[allow(non_upper_case_globals)] @@ -176,7 +203,7 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - type InstFnPtr = unsafe #abi fn (#params_default) #output; unsafe #abi fn initial_fn (#(#param_ty_list),*) #output { use std::ffi::CStr; - match #fn_name.try_link() { + match #fn_name.#try_link_call() { Ok(function) => {#call_dyn_func}, Err(err) => panic!("{}", err), } @@ -195,11 +222,11 @@ fn parse_fn(abi: &syn::Abi, fn_item: syn::ForeignItemFn, attr_data: &AttrData) - #[inline] #vis unsafe #abi fn #fn_name (#(#param_ty_list),*) #output { // InstFnPtr: instance function pointer type - type InstFnPtr = #abi fn (#params_default) #output; - #abi fn initial_fn (#(#param_ty_list),*) #output { + type InstFnPtr = #abi fn (#(#internal_param_ty_list),*) #output; + #abi fn initial_fn (#(#internal_param_ty_list),*) #output { use std::ffi::CStr; - match DYN_FUNC.try_link() { - Ok(function) => {function(#(#param_list),*)}, + match DYN_FUNC.#try_link_call() { + Ok(function) => {function(#(#internal_param_list),*)}, Err(err) => panic!("{}", err), } } diff --git a/src/lazyfn.rs b/src/lazyfn.rs index 289e121..bf6bac7 100644 --- a/src/lazyfn.rs +++ b/src/lazyfn.rs @@ -16,7 +16,7 @@ use std::{ }, }; -use crate::error; +use crate::{error, DylinkResult}; struct DefaultLinker; @@ -72,6 +72,9 @@ impl<'a, F: Copy + Sync + Send> LazyFn<'a, F> { /// for windows, or `dlsym`, and `dlopen` for unix. This function is used by the /// [dylink](dylink_macro::dylink) macro by default. /// If successful, stores address in current instance and returns a reference of the stored value. + /// + /// # Errors + /// If the library fails to link, like if it can't find the library or function, then an error is returned. /// # Example /// ```rust /// # use dylink::dylink; @@ -87,16 +90,26 @@ impl<'a, F: Copy + Sync + Send> LazyFn<'a, F> { /// } /// } /// ``` - pub fn try_link(&self) -> crate::Result<&F> { + pub fn try_link(&self) -> DylinkResult<&F> { self.try_link_with::() } /// Provides a generic argument to supply a user defined linker loader to load the library. /// If successful, stores address in current instance and returns a reference of the stored value. - pub(crate) fn try_link_with(&self) -> crate::Result<&F> { + /// + /// # Errors + /// If the library fails to link, like if it can't find the library or function, then an error is returned. + pub fn try_link_with(&self) -> DylinkResult<&F> + where + L::Data: Send + Sync, + { self.once.call_once(|| { let maybe = match self.link_ty { - LinkType::Vulkan => unsafe { loader::vulkan_loader(self.fn_name) }, + LinkType::Vulkan => unsafe { + loader::vulkan_loader(self.fn_name).ok_or(error::DylinkError::FnNotFound( + self.fn_name.to_str().unwrap().to_owned(), + )) + }, LinkType::General(lib_list) => { let mut errors = vec![]; lib_list @@ -106,12 +119,14 @@ impl<'a, F: Copy + Sync + Send> LazyFn<'a, F> { .map_err(|e| errors.push(e)) .ok() }) - .ok_or_else(|| { - let err: String = errors - .iter() - .map(|e| e.to_string() + "\n") - .collect(); - error::DylinkError::ListNotLoaded(err) + .ok_or_else(|| { + match errors.len() { + 1 => errors[0].clone(), + 2..=usize::MAX => error::DylinkError::ListNotLoaded( + errors.iter().map(|e| e.to_string() + "\n").collect(), + ), + _ => unreachable!(), + } }) } }; @@ -130,7 +145,7 @@ impl<'a, F: Copy + Sync + Send> LazyFn<'a, F> { }); // `call_once` is blocking, so `self.status` is read-only // by this point. Race conditions shouldn't occur. - match (*self.status.borrow()).clone() { + match self.status.clone().take() { None => Ok(self.load(Ordering::Acquire)), Some(err) => Err(err), } @@ -148,6 +163,8 @@ impl<'a, F: Copy + Sync + Send> LazyFn<'a, F> { } } +// should this be removed in favor of just calling load? + impl std::ops::Deref for LazyFn<'_, F> { type Target = F; /// Dereferences the value atomically. diff --git a/src/lazyfn/loader.rs b/src/lazyfn/loader.rs index 2d46a1f..fa98d34 100644 --- a/src/lazyfn/loader.rs +++ b/src/lazyfn/loader.rs @@ -1,31 +1,19 @@ // Copyright (c) 2023 Jonathan "Razordor" Alan Thomason -use std::{ffi, mem, sync::RwLock}; +use std::{ffi, marker::PhantomData, mem, sync::RwLock}; -use crate::{error::*, vulkan, FnPtr, Result}; +use crate::{error::*, vulkan, DylinkResult, FnPtr}; -pub(crate) unsafe fn vulkan_loader(fn_name: &ffi::CStr) -> Result { - let mut maybe_fn = match fn_name.to_bytes() { - b"vkGetInstanceProcAddr" => { - Some(mem::transmute::( - vulkan::vkGetInstanceProcAddr, - )) - } - b"vkGetDeviceProcAddr" => Some(mem::transmute::( - vulkan::vkGetDeviceProcAddr, - )), - _ => None, - }; - maybe_fn = match maybe_fn { - Some(addr) => return Ok(addr), - None => crate::VK_DEVICE - .read() - .expect("failed to get read lock") - .iter() - .find_map(|device| vulkan::vkGetDeviceProcAddr(*device, fn_name.as_ptr() as *const _)), - }; +pub(crate) unsafe fn vulkan_loader(fn_name: &ffi::CStr) -> Option { + let mut maybe_fn = crate::VK_DEVICE + .read() + .expect("failed to get read lock") + .iter() + .find_map(|device| { + vulkan::vkGetDeviceProcAddr(*device, fn_name.as_ptr() as *const ffi::c_char) + }); maybe_fn = match maybe_fn { - Some(addr) => return Ok(addr), + Some(addr) => return Some(addr), None => crate::VK_INSTANCE .read() .expect("failed to get read lock") @@ -35,14 +23,11 @@ pub(crate) unsafe fn vulkan_loader(fn_name: &ffi::CStr) -> Result { }), }; match maybe_fn { - Some(addr) => Ok(addr), + Some(addr) => Some(addr), None => vulkan::vkGetInstanceProcAddr( - vulkan::VkInstance(std::ptr::null_mut()), + vulkan::VkInstance::null(), fn_name.as_ptr() as *const ffi::c_char, - ) - .ok_or(DylinkError::FnNotFound( - fn_name.to_str().unwrap().to_owned(), - )), + ), } } @@ -50,35 +35,38 @@ pub(crate) unsafe fn vulkan_loader(fn_name: &ffi::CStr) -> Result { pub(crate) fn general_loader( lib_name: &ffi::CStr, fn_name: &ffi::CStr, -) -> Result { - use std::collections::HashMap; - - use once_cell::sync::Lazy; - - static DLL_DATA: RwLock>> = - RwLock::new(Lazy::new(HashMap::default)); +) -> DylinkResult +where + L::Data: Send + Sync, +{ + static DLL_DATA: RwLock)>> = + RwLock::new(Vec::new()); // somehow rust is smart enough to infer that maybe_fn is assigned to only once after branching. let maybe_fn; let read_lock = DLL_DATA.read().unwrap(); - if let Some(handle) = read_lock.get(lib_name) { - maybe_fn = L::load_sym(handle, fn_name); - } else { - mem::drop(read_lock); + match read_lock.binary_search_by_key(&lib_name, |(k, _)| k) { + Ok(index) => { + let handle = crate::LibHandle::(read_lock[index].1 .0.cast(), PhantomData); + maybe_fn = L::load_sym(&handle, fn_name) + } + Err(index) => { + mem::drop(read_lock); - let lib_handle = L::load_lib(lib_name); + let lib_handle = L::load_lib(lib_name); - if lib_handle.is_invalid() { - return Err(DylinkError::LibNotLoaded( - lib_name.to_string_lossy().into_owned(), - )); - } else { - maybe_fn = L::load_sym(&lib_handle, fn_name); - DLL_DATA - .write() - .unwrap() - .insert(lib_name.to_owned(), lib_handle); + if lib_handle.is_invalid() { + return Err(DylinkError::LibNotLoaded( + lib_name.to_string_lossy().into_owned(), + )); + } else { + maybe_fn = L::load_sym(&lib_handle, fn_name); + DLL_DATA + .write() + .unwrap() + .insert(index, (lib_name.to_owned(), lib_handle.as_opaque())); + } } } match maybe_fn { diff --git a/src/lazyfn/unix.rs b/src/lazyfn/unix.rs index e2ac6fc..ce1ff6f 100644 --- a/src/lazyfn/unix.rs +++ b/src/lazyfn/unix.rs @@ -13,10 +13,25 @@ extern "C" { } impl crate::RTLinker for DefaultLinker { - fn load_lib(lib_name: &CStr) -> LibHandle { - unsafe { LibHandle(dlopen(lib_name.as_ptr(), RTLD_NOW | RTLD_LOCAL)) } + type Data = c_void; + fn load_lib(lib_name: &CStr) -> LibHandle<'static, Self::Data> { + unsafe { + let result = dlopen(lib_name.as_ptr(), RTLD_NOW | RTLD_LOCAL); + LibHandle::from(result.as_ref()) + } } - fn load_sym(lib_handle: &LibHandle, fn_name: &CStr) -> Option { - unsafe { dlsym(lib_handle.0.cast_mut(), fn_name.as_ptr()) } + fn load_sym( + lib_handle: &LibHandle<'static, Self::Data>, + fn_name: &CStr, + ) -> Option { + unsafe { + dlsym( + lib_handle + .as_ref() + .map(|r| r as *const _ as *mut c_void) + .unwrap_or(std::ptr::null_mut()), + fn_name.as_ptr(), + ) + } } } diff --git a/src/lazyfn/win32.rs b/src/lazyfn/win32.rs index 045839c..f6b9308 100644 --- a/src/lazyfn/win32.rs +++ b/src/lazyfn/win32.rs @@ -18,7 +18,8 @@ extern "system" { } impl crate::RTLinker for DefaultLinker { - fn load_lib(lib_name: &ffi::CStr) -> LibHandle { + type Data = ffi::c_void; + fn load_lib(lib_name: &ffi::CStr) -> LibHandle<'static, Self::Data> { let wide_str: Vec = lib_name .to_string_lossy() .encode_utf16() @@ -32,9 +33,44 @@ impl crate::RTLinker for DefaultLinker { LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, ) }; - LibHandle(result) + LibHandle::from(unsafe { result.as_ref() }) } - fn load_sym(lib_handle: &LibHandle, fn_name: &ffi::CStr) -> Option { - unsafe { GetProcAddress(lib_handle.0.cast_mut(), fn_name.as_ptr().cast()) } + fn load_sym( + lib_handle: &LibHandle<'static, Self::Data>, + fn_name: &ffi::CStr, + ) -> Option { + unsafe { + GetProcAddress( + lib_handle + .as_ref() + .map(|r| r as *const _ as *mut ffi::c_void) + .unwrap_or(std::ptr::null_mut()), + fn_name.as_ptr().cast(), + ) + } } } + +#[test] +fn test_win32_macro_linker() { + extern crate self as dylink; + #[dylink::dylink(name = "Kernel32.dll", strip = true, linker=DefaultLinker)] + extern "stdcall" { + fn SetLastError(_: u32); + } + + // macro output: function + #[dylink::dylink(name = "Kernel32.dll", strip = false, linker=DefaultLinker)] + extern "C" { + fn GetLastError() -> u32; + } + + unsafe { + // static variable has crappy documentation, but can be use for library induction. + match SetLastError.try_link() { + Ok(f) => f(53), + Err(e) => panic!("{}", e), + } + assert_eq!(GetLastError(), 53); + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e43e643..c173275 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! The internal copies of the instance and device handles are only stored until destroyed through a dylink generated vulkan function. //! //! *Note: Due to how dylink handles loading, `vkCreateInstance`, `vkDestroyInstance`, `vkCreateDevice`, and `vkDestroyDevice` are -//! incompatible with the `strip=true` macro argument.* +//! incompatible with the `strip=true` macro argument with the `#[dylink(vulkan)]` specialization.* //! ```rust //! use dylink::dylink; //! @@ -92,10 +92,12 @@ //! Shared library unloading is extremely cursed, always unsafe, and we don't even try to support it. //! Unloading a library means not only are all loaded dylink functions invalidated, but functions loaded from **ALL** //! crates in the project are also invalidated, which will immediately lead to segfaults... a lot of them. +//! +//! *An unloader may be considered in future revisions, but the current abstraction is unsuitable for RAII unloading.* -use std::{collections::HashSet, sync}; +use std::marker::PhantomData; +use std::sync; -use once_cell::sync::Lazy; use std::ffi; mod error; @@ -121,20 +123,19 @@ compile_error!("Dylink Error: Wasm is unsupported."); // These globals are read every time a vulkan function is called for the first time, // which occurs through `LazyFn::link`. -static VK_INSTANCE: sync::RwLock>> = - sync::RwLock::new(Lazy::new(HashSet::new)); +static VK_INSTANCE: sync::RwLock> = sync::RwLock::new(Vec::new()); + +static VK_DEVICE: sync::RwLock> = sync::RwLock::new(Vec::new()); -static VK_DEVICE: sync::RwLock>> = - sync::RwLock::new(Lazy::new(HashSet::new)); +/// Used as a placeholder function pointer. +/// +/// This should **NEVER** be called directly, and promptly cast into the correct function pointer type. +pub type FnPtr = unsafe extern "system" fn() -> isize; -// Used as a placeholder function pointer. This should **NEVER** be called directly, -// and promptly cast into the correct function pointer type. -pub(crate) type FnPtr = unsafe extern "system" fn() -> isize; -// The result of a dylink function -pub(crate) type Result = std::result::Result; +/// The result of a dylink function +pub type DylinkResult = Result; -// TODO: Make the `Global` struct below public when extern types are stable. -// The name `Global` is still TBD. +// TODO: Make the `Global` struct below public when name is picked out /// The global context for specializations. /// @@ -145,7 +146,7 @@ pub(crate) type Result = std::result::Result; pub struct Global; impl Global { // This is safe since vulkan will just discard garbage values - /// Adds an instance to the internal HashSet. + /// Adds an instance to the internal Vec. /// /// Returns whether the instance was newly inserted. That is: /// @@ -156,7 +157,13 @@ impl Global { pub fn insert_instance(&self, instance: vulkan::VkInstance) -> bool { //println!("insert_instance called!"); let mut write_lock = VK_INSTANCE.write().unwrap(); - write_lock.insert(instance) + match write_lock.binary_search(&instance) { + Ok(_) => false, + Err(index) => { + write_lock.insert(index, instance); + true + } + } } /// Removes an instance from the set. Returns whether the instance was present in the set. @@ -165,11 +172,17 @@ impl Global { pub unsafe fn remove_instance(&self, instance: &vulkan::VkInstance) -> bool { //println!("remove_instance called!"); let mut write_lock = VK_INSTANCE.write().unwrap(); - write_lock.remove(instance) + match write_lock.binary_search(instance) { + Ok(index) => { + write_lock.remove(index); + true + } + Err(_) => false, + } } // This is safe since vulkan will just discard garbage values - /// Adds a device to the internal HashSet. + /// Adds a device to the internal Vec. /// /// Returns whether the device was newly inserted. That is: /// @@ -180,7 +193,13 @@ impl Global { pub fn insert_device(&self, device: vulkan::VkDevice) -> bool { //println!("insert_device called!"); let mut write_lock = VK_DEVICE.write().unwrap(); - write_lock.insert(device) + match write_lock.binary_search(&device) { + Ok(_) => false, + Err(index) => { + write_lock.insert(index, device); + true + } + } } /// Removes a device from the set. Returns whether the value was present in the set. @@ -189,24 +208,58 @@ impl Global { pub unsafe fn remove_device(&self, device: &vulkan::VkDevice) -> bool { //println!("remove_device called!"); let mut write_lock = VK_DEVICE.write().unwrap(); - write_lock.remove(device) + match write_lock.binary_search(device) { + Ok(index) => { + write_lock.remove(index); + true + } + Err(_) => false, + } } } -/// Opaque pointer sized library handle -#[repr(transparent)] -pub(crate) struct LibHandle(*const ffi::c_void); -unsafe impl Send for LibHandle {} -unsafe impl Sync for LibHandle {} +// LibHandle is thread-safe because it's inherently immutable, therefore don't add mutable accessors. + +/// Library handle for [RTLinker] +pub struct LibHandle<'a, T: ?Sized>(*const T, PhantomData<&'a ()>); +unsafe impl Send for LibHandle<'_, T> where T: Send {} +unsafe impl Sync for LibHandle<'_, T> where T: Sync {} -impl LibHandle { +impl<'a, T> LibHandle<'a, T> { #[inline] - fn is_invalid(&self) -> bool { + pub fn is_invalid(&self) -> bool { self.0.is_null() } + // This is basically a clone to an opaque handle + pub(crate) fn as_opaque<'b>(&'a self) -> LibHandle<'b, ffi::c_void> { + LibHandle(self.0.cast(), PhantomData) + } + pub fn as_ref(&self) -> Option<&T> { + unsafe { self.0.as_ref() } + } +} + +impl<'a, T> From> for LibHandle<'a, T> { + fn from(value: Option<&'a T>) -> Self { + value + .map(|r| Self((r as *const T).cast(), PhantomData)) + .unwrap_or(Self(std::ptr::null(), PhantomData)) + } +} + +impl<'a, T> From<&'a T> for LibHandle<'a, T> { + fn from(value: &'a T) -> Self { + Self((value as *const T).cast(), PhantomData) + } } -pub(crate) trait RTLinker { - fn load_lib(lib_name: &ffi::CStr) -> LibHandle; - fn load_sym(lib_handle: &LibHandle, fn_name: &ffi::CStr) -> Option; +/// Used to specify a custom run-time linker loader for [LazyFn] +pub trait RTLinker { + type Data; + fn load_lib(lib_name: &ffi::CStr) -> LibHandle<'static, Self::Data> + where + Self::Data: Send + Sync; + fn load_sym(lib_handle: &LibHandle<'static, Self::Data>, fn_name: &ffi::CStr) -> Option + where + Self::Data: Send + Sync; } diff --git a/src/vulkan.rs b/src/vulkan.rs index 6f5d957..b24f8a4 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -13,15 +13,20 @@ extern crate self as dylink; #[doc(hidden)] #[repr(transparent)] -#[derive(Clone, Copy, Hash, PartialEq, Eq)] -pub struct VkInstance(pub(crate) *mut ffi::c_void); +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct VkInstance(*mut ffi::c_void); unsafe impl Sync for VkInstance {} unsafe impl Send for VkInstance {} +impl VkInstance { + pub const fn null() -> Self { + Self(std::ptr::null_mut()) + } +} #[doc(hidden)] #[repr(transparent)] -#[derive(Clone, Copy, Hash, PartialEq, Eq)] -pub struct VkDevice(pub(crate) *mut ffi::c_void); +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct VkDevice(*mut ffi::c_void); unsafe impl Sync for VkDevice {} unsafe impl Send for VkDevice {} @@ -53,9 +58,6 @@ extern "system" { #[allow(non_camel_case_types)] pub(crate) type PFN_vkGetDeviceProcAddr = unsafe extern "system" fn(VkDevice, *const ffi::c_char) -> Option; -#[allow(non_camel_case_types)] -pub(crate) type PFN_vkGetInstanceProcAddr = - unsafe extern "system" fn(VkInstance, *const ffi::c_char) -> Option; // vkGetDeviceProcAddr must be implemented manually to avoid recursion #[allow(non_snake_case)] diff --git a/tests/general_tests.rs b/tests/general_tests.rs index 8762037..a2d2a32 100644 --- a/tests/general_tests.rs +++ b/tests/general_tests.rs @@ -11,9 +11,7 @@ fn test_win32_kernel32() { // macro output: function #[dylink(name = "Kernel32.dll", strip = false)] - extern "C" { - fn GetLastError() -> u32; - } + extern "system" fn GetLastError() -> u32; unsafe { // static variable has crappy documentation, but can be use for library induction. @@ -25,6 +23,29 @@ fn test_win32_kernel32() { } } +#[cfg(windows)] +#[test] +fn test_win32_impl() { + + #[repr(transparent)] + struct Foo(u32); + + // TODO: Self and self (by itself) are impossible to implement, so consider giving hard errors. + impl Foo { + #[dylink(name = "Kernel32.dll")] + extern "stdcall" fn SetLastError(self: Foo); + + #[dylink(name = "Kernel32.dll")] + extern "system" fn GetLastError() -> Foo; + } + + let foo = Foo(23); + unsafe { + foo.SetLastError(); + assert!(Foo::GetLastError().0 == 23) + } +} + // tbh I don't know why this test passes. #[cfg(windows)] #[test] @@ -121,3 +142,24 @@ fn test_multiple_lib_panic() { } } } + + +#[cfg(unix)] +#[test] +fn test_unix_libc() { + #[cfg_attr(target_os = "linux", dylink(name = "libc.so.6", strip=true))] + #[cfg_attr(target_os = "macos", dylink(name = "libc.dylib", strip=true))] + extern "C" { + fn foo(); + } + + match foo.try_link() { + Ok(_) => unreachable!(), + Err(DylinkError::FnNotFound(err)) => { + println!("{err}") + } + Err(DylinkError::LibNotLoaded(err)) => panic!("e0\n{err}"), + Err(DylinkError::ListNotLoaded(err)) => panic!("e1\n{err}"), + Err(_) => todo!() + } +} \ No newline at end of file