diff --git a/.github/workflows/bevy_mod_scripting.yml b/.github/workflows/bevy_mod_scripting.yml index 98bbec5ce1..5ea4fbd012 100644 --- a/.github/workflows/bevy_mod_scripting.yml +++ b/.github/workflows/bevy_mod_scripting.yml @@ -3,15 +3,9 @@ on: branches: - main - staging - paths-ignore: - - '.github/workflows/release-plz.yml' - - 'docs/**' pull_request: branches: - "**" - paths-ignore: - - '.github/workflows/release-plz.yml' - - 'docs/**' name: CI @@ -31,6 +25,27 @@ concurrency: cancel-in-progress: true jobs: + + check-needs-run: + outputs: + any-changes: ${{ steps.changes.outputs.src }} + permissions: + pull-requests: read + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: changes + with: + base: main + filters: | + src: + - 'src/**' + - 'docs/**' + - '.github/workflows/bevy_mod_scripting.yml' + + generate-job-matrix: runs-on: ubuntu-latest # container: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest @@ -49,13 +64,14 @@ jobs: echo "matrix=$(cat matrix-one-line.json)" >> $GITHUB_OUTPUT check: + needs: [check-needs-run] permissions: pull-requests: write contents: write issues: write name: Check - ${{ matrix.run_args.name }} runs-on: ${{ matrix.run_args.os }} - # container: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + if: ${{ needs.check-needs-run.outputs.any-changes == 'true' }} needs: - generate-job-matrix strategy: diff --git a/Cargo.toml b/Cargo.toml index 5c861c3ecb..89ef8c1950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,11 +57,14 @@ bevy_mod_scripting_lua = { path = "crates/languages/bevy_mod_scripting_lua", ver bevy_mod_scripting_rhai = { path = "crates/languages/bevy_mod_scripting_rhai", version = "0.9.3", optional = true } # bevy_mod_scripting_rune = { path = "crates/languages/bevy_mod_scripting_rune", version = "0.9.0-alpha.2", optional = true } bevy_mod_scripting_functions = { workspace = true } +bevy_mod_scripting_derive = { workspace = true } + [workspace.dependencies] profiling = { version = "1.0" } bevy = { version = "0.15.1", default-features = false } bevy_mod_scripting_core = { path = "crates/bevy_mod_scripting_core", version = "0.9.3" } bevy_mod_scripting_functions = { path = "crates/bevy_mod_scripting_functions", version = "0.9.3", default-features = false } +bevy_mod_scripting_derive = { path = "crates/bevy_mod_scripting_derive", version = "0.9.3" } # test utilities script_integration_test_harness = { path = "crates/script_integration_test_harness" } @@ -85,6 +88,7 @@ members = [ "crates/bevy_mod_scripting_functions", "crates/xtask", "crates/script_integration_test_harness", + "crates/bevy_mod_scripting_derive", ] resolver = "2" exclude = ["crates/bevy_api_gen", "crates/macro_tests"] diff --git a/crates/bevy_mod_scripting_core/Cargo.toml b/crates/bevy_mod_scripting_core/Cargo.toml index e81d874260..f657f88b68 100644 --- a/crates/bevy_mod_scripting_core/Cargo.toml +++ b/crates/bevy_mod_scripting_core/Cargo.toml @@ -41,6 +41,8 @@ smallvec = "1.11" itertools = "0.13" derivative = "2.2" profiling = { workspace = true } +bevy_mod_scripting_derive = { workspace = true } + [dev-dependencies] test_utils = { workspace = true } tokio = { version = "1", features = ["rt", "macros"] } diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs b/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs index f7d3b4ff7d..1f4eb9106a 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/mod.rs @@ -12,15 +12,65 @@ pub mod type_dependencies; #[cfg(test)] #[allow(dead_code)] mod test { - use bevy::reflect::{FromReflect, GetTypeRegistration, Typed}; + use bevy::reflect::{FromReflect, GetTypeRegistration, Reflect, Typed}; + use bevy_mod_scripting_derive::script_bindings; use crate::{ - bindings::function::from::{Ref, Val}, + bindings::function::{ + from::{Ref, Val}, + namespace::IntoNamespace, + script_function::AppScriptFunctionRegistry, + }, error::InteropError, }; use super::arg_meta::{ScriptArgument, ScriptReturn}; + #[test] + fn test_macro_generates_correct_registrator_function() { + #[derive(Reflect)] + struct TestStruct; + + #[script_bindings(bms_core_path = "crate", name = "test_fn")] + impl TestStruct { + /// My docs !! + fn test_fn(_self: Ref, mut _arg1: usize) {} + } + + let mut test_world = bevy::ecs::world::World::default(); + + register_test_fn(&mut test_world); + + let app_registry = test_world + .get_resource::() + .unwrap(); + let app_registry = app_registry.read(); + + let test_fn = app_registry + .get_function(TestStruct::into_namespace(), "test_fn") + .unwrap(); + + assert_eq!(test_fn.info.docs, Some("My docs !!".into())); + assert_eq!(test_fn.info.arg_info.len(), 2); + + assert_eq!( + test_fn.info.arg_info[0].type_id, + std::any::TypeId::of::>() + ); + assert_eq!(test_fn.info.arg_info[0].name, Some("_self".into())); + + assert_eq!( + test_fn.info.arg_info[1].type_id, + std::any::TypeId::of::() + ); + assert_eq!(test_fn.info.arg_info[1].name, Some("_arg1".into())); + + assert_eq!( + test_fn.info.return_info.type_id, + std::any::TypeId::of::<()>() + ); + } + fn test_is_valid_return() {} fn test_is_valid_arg() {} fn test_is_valid_arg_and_return() {} diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/namespace.rs b/crates/bevy_mod_scripting_core/src/bindings/function/namespace.rs index e255798688..e9f4d5b30f 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/namespace.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/namespace.rs @@ -98,6 +98,35 @@ impl<'a, S: IntoNamespace> NamespaceBuilder<'a, S> { /// Registers a function in the namespace pub fn register<'env, N, F, M>(&mut self, name: N, function: F) -> &mut Self + where + N: Into>, + F: ScriptFunction<'env, M> + GetFunctionTypeDependencies + GetFunctionInfo, + { + self.register_inner(name, function, None, None) + } + + /// Registers a function in the namespace with a docstring + pub fn register_documented<'env, N, F, M>( + &mut self, + name: N, + function: F, + docstring: &'static str, + arg_names: &'static [&'static str], + ) -> &mut Self + where + N: Into>, + F: ScriptFunction<'env, M> + GetFunctionTypeDependencies + GetFunctionInfo, + { + self.register_inner(name, function, Some(docstring), Some(arg_names)) + } + + fn register_inner<'env, N, F, M>( + &mut self, + name: N, + function: F, + docstring: Option<&'static str>, + arg_names: Option<&'static [&'static str]>, + ) -> &mut Self where N: Into>, F: ScriptFunction<'env, M> + GetFunctionTypeDependencies + GetFunctionInfo, @@ -108,7 +137,13 @@ impl<'a, S: IntoNamespace> NamespaceBuilder<'a, S> { .world .get_resource_or_init::(); let mut registry = registry.write(); - registry.register(S::into_namespace(), name, function); + registry.register_with_arg_names( + S::into_namespace(), + name, + function, + docstring.unwrap_or_default(), + arg_names.unwrap_or(&[]), + ); } { let type_registry = self.world.get_resource_or_init::(); diff --git a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs index 635c2c08f8..c2b1dd559c 100644 --- a/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs +++ b/crates/bevy_mod_scripting_core/src/bindings/function/script_function.rs @@ -320,47 +320,63 @@ impl ScriptFunctionRegistry { ) where F: ScriptFunction<'env, M> + GetFunctionInfo, { - self.register_overload(namespace, name, func, false, None::<&'static str>); + self.register_overload(namespace, name, func, false, None::<&'static str>, None); } /// Equivalent to [`ScriptFunctionRegistry::register`] but with the ability to provide documentation for the function. /// /// The docstring will be added to the function's metadata and can be accessed at runtime. - pub fn register_documented( + pub fn register_documented<'env, F, M>( &mut self, namespace: Namespace, name: impl Into>, func: F, docs: &'static str, ) where - F: ScriptFunction<'static, M> + GetFunctionInfo, + F: ScriptFunction<'env, M> + GetFunctionInfo, + { + self.register_overload(namespace, name, func, false, Some(docs), None); + } + + /// Equivalent to [`ScriptFunctionRegistry::register`] but with the ability to provide argument names for the function as well as documentation. + /// + /// The argument names and docstring will be added to the function's metadata and can be accessed at runtime. + pub fn register_with_arg_names<'env, F, M>( + &mut self, + namespace: Namespace, + name: impl Into>, + func: F, + docs: &'static str, + arg_names: &'static [&'static str], + ) where + F: ScriptFunction<'env, M> + GetFunctionInfo, { - self.register_overload(namespace, name, func, false, Some(docs)); + self.register_overload(namespace, name, func, false, Some(docs), Some(arg_names)); } /// Overwrite a function with the given name. If the function does not exist, it will be registered as a new function. - pub fn overwrite( + pub fn overwrite<'env, F, M>( &mut self, namespace: Namespace, name: impl Into>, func: F, ) where - F: ScriptFunction<'static, M> + GetFunctionInfo, + F: ScriptFunction<'env, M> + GetFunctionInfo, { - self.register_overload(namespace, name, func, true, None::<&'static str>); + self.register_overload(namespace, name, func, true, None::<&'static str>, None); } /// Equivalent to [`ScriptFunctionRegistry::overwrite`] but with the ability to provide documentation for the function. - pub fn overwrite_documented( + pub fn overwrite_documented<'env, F, M>( &mut self, namespace: Namespace, name: impl Into>, func: F, docs: &'static str, ) where - F: ScriptFunction<'static, M> + GetFunctionInfo, + F: ScriptFunction<'env, M> + GetFunctionInfo, { - self.register_overload(namespace, name, func, true, Some(docs)); + self.register_overload(namespace, name, func, true, Some(docs), None); } /// Remove a function from the registry if it exists. Returns the removed function if it was found. @@ -401,6 +417,7 @@ impl ScriptFunctionRegistry { func: F, overwrite: bool, docs: Option>>, + arg_names: Option<&'static [&'static str]>, ) where F: ScriptFunction<'env, M> + GetFunctionInfo, { @@ -413,6 +430,10 @@ impl ScriptFunctionRegistry { Some(docs) => info.with_docs(docs.into()), None => info, }; + let info = match arg_names { + Some(arg_names) => info.with_arg_names(arg_names), + None => info, + }; let func = func.into_dynamic_script_function().with_info(info); self.functions.insert(FunctionKey { name, namespace }, func); return; diff --git a/crates/bevy_mod_scripting_core/src/docgen/info.rs b/crates/bevy_mod_scripting_core/src/docgen/info.rs index 8633bb5f37..82f4ffaea1 100644 --- a/crates/bevy_mod_scripting_core/src/docgen/info.rs +++ b/crates/bevy_mod_scripting_core/src/docgen/info.rs @@ -78,6 +78,20 @@ impl FunctionInfo { self.docs = Some(docs.into()); self } + + /// Add argument names to the function info. + /// + /// If the number of argument names is less than the number of arguments, the remaining arguments will be unnamed. + /// If the number of argument names is greater than the number of arguments, the extra argument names will be ignored. + pub fn with_arg_names(mut self, arg_names: &[&'static str]) -> Self { + self.arg_info + .iter_mut() + .zip(arg_names.iter()) + .for_each(|(arg, name)| { + arg.name = Some(Cow::Borrowed(*name)); + }); + self + } } #[derive(Debug, Clone, PartialEq, Reflect)] diff --git a/crates/bevy_mod_scripting_derive/Cargo.toml b/crates/bevy_mod_scripting_derive/Cargo.toml new file mode 100644 index 0000000000..fdf050b803 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_mod_scripting_derive" +version = "0.9.3" +edition = "2021" +authors = ["Maksymilian Mozolewski "] +license = "MIT OR Apache-2.0" +description = "Necessary functionality for Lua support with bevy_mod_scripting" +repository = "https://github.com/makspll/bevy_mod_scripting" +homepage = "https://github.com/makspll/bevy_mod_scripting" +keywords = ["bevy", "gamedev", "scripting", "rhai"] +categories = ["game-development"] +readme = "readme.md" + +[dependencies] +syn = "2" +proc-macro2 = "1" +quote = "1" + +[lib] +proc-macro = true + +[lints] +workspace = true diff --git a/crates/bevy_mod_scripting_derive/readme.md b/crates/bevy_mod_scripting_derive/readme.md new file mode 100644 index 0000000000..e058ed4f00 --- /dev/null +++ b/crates/bevy_mod_scripting_derive/readme.md @@ -0,0 +1,3 @@ +# bevy_mod_scripting_lua_derive + +This crate is a part of the ["bevy_mod_scripting" workspace](https://github.com/makspll/bevy_mod_scripting). \ No newline at end of file diff --git a/crates/bevy_mod_scripting_derive/src/lib.rs b/crates/bevy_mod_scripting_derive/src/lib.rs new file mode 100644 index 0000000000..d12271279e --- /dev/null +++ b/crates/bevy_mod_scripting_derive/src/lib.rs @@ -0,0 +1,193 @@ +//! Derive macros for BMS + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote_spanned, ToTokens}; +use syn::{spanned::Spanned, ImplItemFn, ItemImpl}; + +/// Derive macro for generating script bindings from an impl block. +/// +/// Does not support generics. +/// +/// Arguments: +/// - `name`: the name to use to suffix the generated function, i.e. `test_fn` will generate `register_test_fn. Defaults to `functions` +/// - `remote`: If true the original impl block will be ignored, and only the function registrations will be generated +/// - `bms_core_path`: If set the path to override bms imports, normally only used internally +#[proc_macro_attribute] +pub fn script_bindings( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let args = syn::parse_macro_input!(args as Args); + + let impl_block = syn::parse_macro_input!(input as ItemImpl); + let impl_span = impl_block.span(); + // let (impl_generics, ty_generics, where_clause) = impl_block.generics.split_for_impl(); + + let type_ident_with_generics = &impl_block.self_ty; + let mut function_registrations = Vec::with_capacity(impl_block.items.len()); + for i in &impl_block.items { + match i { + syn::ImplItem::Fn(impl_item_fn) => { + let fun = process_impl_fn(impl_item_fn); + function_registrations.push(fun); + } + _ => continue, + } + } + + let impl_block = match args.remote { + true => TokenStream::default(), + false => quote_spanned! {impl_span=> + #impl_block + }, + }; + + let bms_core_path = &args.bms_core_path; + + let function_name = format_ident!("register_{}", args.name); + + let out = quote_spanned! {impl_span=> + fn #function_name(world: &mut bevy::ecs::world::World) { + #bms_core_path::bindings::function::namespace::NamespaceBuilder::<#type_ident_with_generics>::new(world) + #(#function_registrations)*; + } + + #impl_block + }; + + out.into() +} + +struct Args { + /// The name to use to suffix the generated function, i.e. `test_fn` will generate `register_test_fn + pub name: syn::Ident, + /// If true the original impl block will be ignored, and only the function registrations will be generated + pub remote: bool, + /// If set the path to override bms imports + pub bms_core_path: syn::Path, +} + +impl syn::parse::Parse for Args { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // parse separated key-value pairs + let pairs = + syn::punctuated::Punctuated::::parse_terminated(input)?; + + let mut name = syn::Ident::new("functions", Span::call_site()); + let mut remote = false; + let mut bms_core_path = + syn::Path::from(syn::Ident::new("bevy_mod_scripting", Span::call_site())); + bms_core_path.segments.push(syn::PathSegment { + ident: syn::Ident::new("core", Span::call_site()), + arguments: syn::PathArguments::None, + }); + let mut unknown_spans = Vec::default(); + for pair in pairs { + match &pair { + syn::Meta::Path(path) => { + if path.is_ident("remote") { + remote = true; + continue; + } + } + syn::Meta::NameValue(name_value) => { + if name_value.path.is_ident("bms_core_path") { + if let syn::Expr::Lit(path) = &name_value.value { + if let syn::Lit::Str(lit_str) = &path.lit { + bms_core_path = syn::parse_str(&lit_str.value())?; + continue; + } + } + } else if name_value.path.is_ident("name") { + if let syn::Expr::Lit(path) = &name_value.value { + if let syn::Lit::Str(lit_str) = &path.lit { + name = syn::parse_str(&lit_str.value())?; + continue; + } + } + } + } + _ => {} + } + + unknown_spans.push(pair.span()); + } + + if !unknown_spans.is_empty() { + return Err(syn::Error::new( + unknown_spans[0], + "Unknown argument to script_bindings", + )); + } + + Ok(Self { + remote, + bms_core_path, + name, + }) + } +} + +fn stringify_pat_type(pat_type: &syn::Pat) -> String { + match pat_type { + syn::Pat::Ident(pat_ident) => pat_ident.ident.to_string(), + syn::Pat::Type(pat_type) => stringify_pat_type(&pat_type.pat), + + p => p.to_token_stream().to_string(), + } +} + +/// Converts an impl block function into a function registration, i.e. a closure which will be used to register this function, as well as +/// the target function reference and other metadata +fn process_impl_fn(fun: &ImplItemFn) -> TokenStream { + let args = &fun.sig.inputs; + let args_names = args.iter().map(|arg| match arg { + syn::FnArg::Receiver(_) => syn::LitStr::new("self", Span::call_site()), + syn::FnArg::Typed(pat_type) => { + syn::LitStr::new(&stringify_pat_type(&pat_type.pat), Span::call_site()) + } + }); + let body = &fun.block; + let docstring = parse_docstring(fun.attrs.iter()) + .map(|s| syn::LitStr::new(&s, Span::call_site())) + .unwrap_or(syn::LitStr::new("", Span::call_site())); + let fun_name = syn::LitStr::new(&fun.sig.ident.to_string(), Span::call_site()); + quote_spanned! {Span::call_site()=> + .register_documented( + #fun_name, + |#args| #body, + #docstring, + &[#(#args_names),*] + ) + } +} + +/// Ideally we'd be doing something like rustdoc: https://github.com/rust-lang/rust/blob/124cc92199ffa924f6b4c7cc819a85b65e0c3984/compiler/rustc_resolve/src/rustdoc.rs#L102 +/// but that is too much complexity, stripping the first space should be good enough for now. +fn parse_docstring<'a>(attrs: impl Iterator) -> Option { + let docs = attrs.filter_map(|attr| { + if attr.path().is_ident("doc") { + if let syn::Meta::NameValue(meta_name_value) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &meta_name_value.value { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + if lit_str.value().len() > 1 { + return Some(lit_str.value()[1..].to_string()); + } else { + return Some(lit_str.value()); + } + } + } + } + }; + + None + }); + + // join with newline + let docs = docs.collect::>(); + if docs.is_empty() { + return None; + } + + Some(docs.join("\n")) +} diff --git a/release-plz.toml b/release-plz.toml index 4ae945ea03..116b184194 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -53,6 +53,10 @@ version_group = "main" name = "bevy_mod_scripting_core" version_group = "main" +[[package]] +name = "bevy_mod_scripting_derive" +version_group = "main" + [[package]] name = "bevy_mod_scripting_rhai" version_group = "main" diff --git a/src/lib.rs b/src/lib.rs index be3001e6e7..ba771f3123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,4 +19,5 @@ pub mod rhai { // pub use bevy_mod_scripting_rune::*; // } +pub use bevy_mod_scripting_derive::*; pub use bevy_mod_scripting_functions::*;