Implement string parameter specialisation

This commit is contained in:
lumi 2025-06-25 18:42:09 +10:00
parent 681dd332ab
commit 98100d02fa
Signed by: luaneko
GPG Key ID: 406809B8763FF07A
4 changed files with 207 additions and 83 deletions

View File

@ -485,12 +485,17 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
self self
} }
pub fn param_str(&mut self, name: impl Display) -> &mut Self { pub fn param_str(
// fast-path for &str and &[u8]-like parameters &mut self,
name: impl Display,
allow_nil: bool,
check_utf8: bool,
) -> &mut Self {
// fast-path for &[u8] and &str-like parameters
// //
// this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len` // this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len`
// arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a // arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a
// temporary cdata to pass the string and its length in one argument // temporary cdata to pass the string and its length in one argument.
let Self { let Self {
lparams, lparams,
cparams, cparams,
@ -499,22 +504,26 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
.. ..
} = self; } = self;
let param_ptr = <*const u8>::cdecl("ptr"); let param_ptr = <*const u8>::cdecl(&name);
let param_len = usize::cdecl("len"); let param_len = usize::cdecl(format!("{name}_len"));
(!lparams.is_empty()).then(|| lparams.push_str(", ")); (!lparams.is_empty()).then(|| lparams.push_str(", "));
(!cparams.is_empty()).then(|| cparams.push_str(", ")); (!cparams.is_empty()).then(|| cparams.push_str(", "));
(!cargs.is_empty()).then(|| cargs.push_str(", ")); (!cargs.is_empty()).then(|| cargs.push_str(", "));
write!(lparams, "{name}").unwrap(); write!(lparams, "{name}").unwrap();
write!(cparams, "{param_ptr}, {param_len}",).unwrap(); write!(cparams, "{param_ptr}, {param_len}").unwrap();
write!(cargs, "{name}, __{name}_len").unwrap(); write!(cargs, "{name}, __{name}_len").unwrap();
write!(prelude, "local __{name}_len = 0; ").unwrap(); write!(prelude, "local __{name}_len = 0; if {name} ~= nil then ").unwrap();
write!( write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap();
prelude, write!(prelude, r#"__{name}_len = #{name}; "#).unwrap();
r#"if {name} ~= nil then assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); __{name}_len = #{name}; end; "# if check_utf8 {
) write!(prelude, r#"assert(__C.{IS_UTF8_FN}({name}, __{name}_len), "argument '{name}' must be a valid utf-8 string"); "#).unwrap();
.unwrap(); }
if !allow_nil {
write!(prelude, r#"else return error("string expected in argument '{name}', got " .. type({name})); "#).unwrap();
}
write!(prelude, r#"end; "#).unwrap();
self self
} }
@ -549,21 +558,21 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
if T::Into::ty() == TypeType::Void { if T::Into::ty() == TypeType::Void {
write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap(); write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap();
} else { } else {
let check = T::postlude("__res"); let check = T::postlude("__ret");
write!(lua, "local __res = __C.{func}({cargs}); ").unwrap(); write!(lua, "local __ret = __C.{func}({cargs}); ").unwrap();
write!(lua, "{check}{postlude}return __res; end").unwrap(); write!(lua, "{check}{postlude}return __ret; end").unwrap();
} }
writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap(); writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap();
} }
FfiReturnConvention::ByOutParam => { FfiReturnConvention::ByOutParam => {
let ct = T::Into::name(); let ct = T::Into::name();
let check = T::postlude("__res"); let check = T::postlude("__out");
write!(lua, "local __res = __new(__ct.{ct}); __C.{func}(__res").unwrap(); write!(lua, "local __out = __new(__ct.{ct}); __C.{func}(__out").unwrap();
if !cargs.is_empty() { if !cargs.is_empty() {
write!(lua, ", {cargs}").unwrap(); write!(lua, ", {cargs}").unwrap();
} }
write!(lua, "); {check}{postlude}return __res; end").unwrap(); write!(lua, "); {check}{postlude}return __out; end").unwrap();
write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("out")).unwrap(); write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("out")).unwrap();
if !cparams.is_empty() { if !cparams.is_empty() {
write!(cdef, ", {cparams}").unwrap(); write!(cdef, ", {cparams}").unwrap();

View File

@ -1,5 +1,6 @@
use crate::utils::{ use crate::utils::{
ffi_crate, is_primitivelike, is_unit, pat_ident, syn_assert, syn_error, ty_name, StringLike, ffi_crate, is_optionlike, is_primitivelike, is_stringlike, is_unit, pat_ident,
syn_assert, syn_error, ty_name,
}; };
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned}; use quote::{ToTokens, format_ident, quote, quote_spanned};
@ -29,12 +30,27 @@ pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> {
)) ))
} }
struct Registry {
ty: Ident,
shims: Vec<ImplItemFn>,
build: Vec<TokenStream>,
}
impl Registry {
fn new(ty: Ident) -> Self {
Self {
ty,
shims: vec![],
build: vec![],
}
}
}
fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> { fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = imp.self_ty.clone(); let ty = imp.self_ty.clone();
let ty_name = ty_name(&ty)?; let ty_name = ty_name(&ty)?;
let mut ffi_funcs = FfiRegistry::new(ty_name.clone()); let mut registry = Registry::new(ty_name.clone());
let mut lua_funcs = LuaRegistry::new(ty_name.clone());
let mut mms = HashSet::new(); let mut mms = HashSet::new();
let mut lua_drop = None; let mut lua_drop = None;
@ -47,7 +63,7 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
add_ffi_function(&mut ffi_funcs, &func)?; add_ffi_function(&mut registry, &func)?;
} }
for func in get_lua_functions(imp)? { for func in get_lua_functions(imp)? {
@ -59,37 +75,32 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
if func.attrs.metamethod == Some(Metamethod::Gc) { if let Some(Metamethod::Gc) = func.attrs.metamethod {
lua_drop = Some(func); lua_drop = Some(func);
} else { } else {
add_lua_function(&mut lua_funcs, &func)?; add_lua_function(&mut registry, &func)?;
} }
} }
if !mms.contains(&Metamethod::New) { if !mms.contains(&Metamethod::New) {
inject_fallback_new(&mut lua_funcs)?; inject_fallback_new(&mut registry)?;
} }
inject_merged_drop(&mut ffi_funcs, lua_drop.as_ref())?; inject_merged_drop(&mut registry, lua_drop.as_ref())?;
let ffi_shims = &ffi_funcs.shims; let shims = &registry.shims;
let ffi_build = &ffi_funcs.build; let build = &registry.build;
let lua_build = &lua_funcs.build; let exports = generate_ffi_exports(&registry)?;
let ffi_exports = generate_ffi_exports(&ffi_funcs)?;
Ok(quote_spanned!(ty.span() => Ok(quote_spanned!(ty.span() =>
impl #ty { #(#ffi_shims)* } impl #ty { #(#shims)* }
unsafe impl #ffi::Metatype for #ty { unsafe impl #ffi::Metatype for #ty {
type Target = Self; type Target = Self;
fn build(b: &mut #ffi::MetatypeBuilder) { #(#build)* }
fn build(b: &mut #ffi::MetatypeBuilder) {
#(#ffi_build)*
#(#lua_build)*
}
} }
#ffi_exports #exports
)) ))
} }
@ -201,9 +212,15 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
&& let Some(ref abi) = abi.name && let Some(ref abi) = abi.name
&& abi.value() == "Lua-C" && abi.value() == "Lua-C"
{ {
syn_assert!(
func.sig.generics.params.len() == 0,
func.sig.generics,
"cannot be generic"
);
func.sig.abi = None; func.sig.abi = None;
let params = func let params: Vec<_> = func
.sig .sig
.inputs .inputs
.iter() .iter()
@ -221,6 +238,24 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
ReturnType::Type(_, ref ty) => (**ty).clone(), ReturnType::Type(_, ref ty) => (**ty).clone(),
}; };
for param in params.iter() {
// double underscores are reserved for generated glue code
syn_assert!(
!pat_ident(&param.pat)?.to_string().starts_with("__"),
param.pat,
"parameter names should not start with `__`"
);
// lifetime should be determined by the caller (lua)
if let Type::Reference(ref ty) = *param.ty {
syn_assert!(
ty.lifetime.is_none(),
ty.lifetime,
"lifetime should be determined by the caller"
);
}
}
let attrs = parse_ffi_function_attrs(&mut func.attrs)?; let attrs = parse_ffi_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm)); attrs.metamethod.map(|mm| document_metamethod(func, mm));
@ -261,14 +296,26 @@ fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAtt
Ok(parsed) Ok(parsed)
} }
#[derive(Debug, Clone, Copy)]
enum FfiParameterType { enum FfiParameterType {
Default, Default,
StringLike(StringLike),
OptionStringLike(StringLike),
} }
fn get_ffi_param_type(_ty: &Type) -> FfiParameterType { fn get_ffi_param_type(ty: &Type) -> FfiParameterType {
FfiParameterType::Default if let Some(str) = is_stringlike(ty) {
FfiParameterType::StringLike(str)
} else if let Some(arg) = is_optionlike(ty)
&& let Some(str) = is_stringlike(arg)
{
FfiParameterType::OptionStringLike(str)
} else {
FfiParameterType::Default
}
} }
#[derive(Debug, Clone, Copy)]
enum FfiReturnType { enum FfiReturnType {
Void, Void,
ByValue, ByValue,
@ -298,23 +345,7 @@ fn get_ffi_ret_type(ty: &Type) -> FfiReturnType {
} }
} }
struct FfiRegistry { fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
ty: Ident,
shims: Vec<ImplItemFn>,
build: Vec<TokenStream>,
}
impl FfiRegistry {
fn new(ty: Ident) -> Self {
Self {
ty,
shims: vec![],
build: vec![],
}
}
}
fn add_ffi_function(registry: &mut FfiRegistry, func: &FfiFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = &registry.ty; let ty = &registry.ty;
let func_name = &func.name; let func_name = &func.name;
@ -366,6 +397,41 @@ fn add_ffi_function(registry: &mut FfiRegistry, func: &FfiFunction) -> Result<()
b.param::<#func_param>(#name); b.param::<#func_param>(#name);
)); ));
} }
ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => {
let shim_param_len = format_ident!("arg{i}_len");
shim_params.push(quote_spanned!(func_param.span() =>
#shim_param: ::std::option::Option<&::std::primitive::u8>,
#shim_param_len: ::std::primitive::usize
));
let allow_nil = matches!(ty, FfiParameterType::OptionStringLike(_));
let check_utf8 = matches!(str, StringLike::Str);
let mut func_arg = quote_spanned!(func_param.span() =>
#shim_param.map(|s| ::std::slice::from_raw_parts(s, #shim_param_len))
);
func_arg = match str {
StringLike::SliceU8 => func_arg,
StringLike::BStr => {
quote_spanned!(func_param.span() => #func_arg.map(::bstr::BStr::new))
}
StringLike::Str => {
quote_spanned!(func_param.span() => #func_arg.map(|s| {
::std::debug_assert!(::std::str::from_utf8(s).is_ok());
::std::str::from_utf8_unchecked(s)
}))
}
};
if !allow_nil {
func_arg = quote_spanned!(func_param.span() => {
let arg = #func_arg;
::std::debug_assert!(arg.is_some());
arg.unwrap_unchecked()
});
}
func_args.push(func_arg);
build.push(quote_spanned!(param.pat.span() =>
b.param_str(#name, #allow_nil, #check_utf8);
));
}
} }
} }
@ -439,7 +505,7 @@ fn add_ffi_function(registry: &mut FfiRegistry, func: &FfiFunction) -> Result<()
Ok(()) Ok(())
} }
fn generate_ffi_exports(registry: &FfiRegistry) -> Result<TokenStream> { fn generate_ffi_exports(registry: &Registry) -> Result<TokenStream> {
let ty = &registry.ty; let ty = &registry.ty;
let names = registry.shims.iter().map(|f| &f.sig.ident); let names = registry.shims.iter().map(|f| &f.sig.ident);
@ -474,6 +540,12 @@ fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
&& let Some(ref abi) = abi.name && let Some(ref abi) = abi.name
&& abi.value() == "Lua" && abi.value() == "Lua"
{ {
syn_assert!(
func.sig.generics.params.len() == 0,
func.sig.generics,
"cannot be generic"
);
let mut params: Vec<_> = func let mut params: Vec<_> = func
.sig .sig
.inputs .inputs
@ -597,18 +669,7 @@ fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAtt
Ok(parsed) Ok(parsed)
} }
struct LuaRegistry { fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
ty: Ident,
build: Vec<TokenStream>,
}
impl LuaRegistry {
fn new(ty: Ident) -> Self {
Self { ty, build: vec![] }
}
}
fn add_lua_function(registry: &mut LuaRegistry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let func_name = &func.name; let func_name = &func.name;
@ -628,7 +689,7 @@ fn add_lua_function(registry: &mut LuaRegistry, func: &LuaFunction) -> Result<()
Ok(()) Ok(())
} }
fn inject_fallback_new(registry: &mut LuaRegistry) -> Result<()> { fn inject_fallback_new(registry: &mut Registry) -> Result<()> {
let ty = &registry.ty; let ty = &registry.ty;
let lua = format!( let lua = format!(
r#"function() error("type '{}' has no constructor"); end"#, r#"function() error("type '{}' has no constructor"); end"#,
@ -642,7 +703,7 @@ fn inject_fallback_new(registry: &mut LuaRegistry) -> Result<()> {
Ok(()) Ok(())
} }
fn inject_merged_drop(registry: &mut FfiRegistry, lua: Option<&LuaFunction>) -> Result<()> { fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let ty = &registry.ty; let ty = &registry.ty;

View File

@ -57,13 +57,13 @@ pub fn is_unit(ty: &Type) -> bool {
pub fn is_primitivelike(ty: &Type) -> bool { pub fn is_primitivelike(ty: &Type) -> bool {
match ty { match ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => true, // unit type Type::Tuple(tuple) if tuple.elems.is_empty() => return true, // unit type
Type::Reference(_) | Type::Ptr(_) => true, Type::Reference(_) | Type::Ptr(_) => return true,
Type::Paren(paren) => is_primitivelike(&paren.elem), Type::Paren(paren) => return is_primitivelike(&paren.elem),
Type::Path(path) => { Type::Path(path) => {
if let Some(name) = path.path.get_ident() { if let Some(name) = path.path.get_ident() {
matches!( return matches!(
format!("{name}").as_str(), name.to_string().as_str(),
"bool" "bool"
| "u8" | "u8"
| "u16" | "u16"
@ -94,11 +94,66 @@ pub fn is_primitivelike(ty: &Type) -> bool {
| "c_size_t" | "c_size_t"
| "c_ssize_t" | "c_ssize_t"
| "c_ptrdiff_t" | "c_ptrdiff_t"
) );
} else {
false
} }
} }
_ => false, _ => {}
}
false
}
#[derive(Debug, Clone, Copy)]
pub enum StringLike {
SliceU8,
Str,
BStr,
}
pub fn is_stringlike(ty: &Type) -> Option<StringLike> {
if let Type::Reference(ty) = ty
&& ty.mutability.is_none()
&& ty.lifetime.is_none()
{
match *ty.elem {
Type::Slice(ref slice) => {
// match &[u8]
if let Type::Path(ref path) = *slice.elem
&& let Some(name) = path.path.get_ident()
&& name == "u8"
{
return Some(StringLike::SliceU8);
}
}
Type::Path(ref path) => {
// match &str or &BStr
if let Some(name) = path.path.get_ident() {
match name.to_string().as_str() {
"str" => return Some(StringLike::Str),
"BStr" => return Some(StringLike::BStr),
_ => {}
}
}
}
_ => {}
}
}
None
}
pub fn is_optionlike(ty: &Type) -> Option<&Type> {
if let Type::Path(path) = ty
&& path.path.leading_colon.is_none()
&& path.path.segments.len() == 1
&& let Some(segment) = path.path.segments.get(0)
&& segment.ident == "Option"
&& let PathArguments::AngleBracketed(ref angle) = segment.arguments
&& angle.args.len() == 1
&& let Some(GenericArgument::Type(ty)) = angle.args.get(0)
{
Some(ty)
} else {
None
} }
} }

View File

@ -1333,8 +1333,7 @@ impl<'s> DerefMut for StackGuard<'s> {
impl<'s> Drop for StackGuard<'s> { impl<'s> Drop for StackGuard<'s> {
fn drop(&mut self) { fn drop(&mut self) {
#[cfg(debug_assertions)] if cfg!(debug_assertions) && self.check_overpop {
if self.check_overpop {
let new_size = self.stack.size(); let new_size = self.stack.size();
assert!( assert!(
self.size <= new_size, self.size <= new_size,