From d15c8b3e299475e846300f6d242476b50d413c87 Mon Sep 17 00:00:00 2001 From: "Arthur G.P. Schuster" Date: Fri, 19 Sep 2025 14:52:31 +0200 Subject: [PATCH] allow calling simple Python functions from Rhai --- Cargo.lock | 64 +++++++++++--------- Cargo.toml | 2 +- src/lib.rs | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 466bad8..061cca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,15 +24,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "const-random" @@ -80,7 +80,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -154,9 +154,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -240,9 +240,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rhai" -version = "1.22.2" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249" +checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594" dependencies = [ "ahash", "bitflags", @@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "rhai-python" -version = "0.1.0" +version = "0.1.1" dependencies = [ "pyo3", "pyo3-build-config", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "rhai_codegen" -version = "2.2.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" dependencies = [ "proc-macro2", "quote", @@ -307,9 +307,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -318,9 +318,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "thin-vec" @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unindent" @@ -363,36 +363,42 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "bitflags", + "wit-bindgen", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c1ef4ba..771f920 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai-python" -version = "0.1.0" +version = "0.1.1" edition = "2021" [lib] diff --git a/src/lib.rs b/src/lib.rs index 7d8f061..cc335b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,7 @@ fn python_to_dynamic(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult, + py_functions: std::collections::HashMap, } #[pymethods] @@ -90,6 +91,7 @@ impl RhaiEngine { Self { engine: Engine::new(), scope: Scope::new(), + py_functions: std::collections::HashMap::new(), } } @@ -150,11 +152,179 @@ impl RhaiEngine { .map(|(name, _, _)| name.to_string()) .collect() } + + /// Register a Python function to be callable from Rhai using signature inspection + fn register_fn(&mut self, py: Python<'_>, name: &str, func: PyObject) -> PyResult<()> { + // Get the function signature using inspect.signature() + let inspect_module = py.import("inspect")?; + let signature_func = inspect_module.getattr("signature")?; + let sig = signature_func.call1((&func,))?; + let parameters = sig.getattr("parameters")?; + let param_count = parameters.len()?; + + // Store the function + self.py_functions + .insert(name.to_string(), func.clone_ref(py)); + + let func_name = name.to_string(); + + // Register the function with the appropriate arity + match param_count { + 0 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move || -> Result> { + Python::with_gil(|py| { + let result = func + .call0(py) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + 1 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move |arg1: Dynamic| -> Result> { + Python::with_gil(|py| { + let py_arg1 = dynamic_to_python(py, arg1) + .map_err(|e| format!("Type conversion error: {}", e))?; + let result = func + .call1(py, (py_arg1,)) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + 2 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move |arg1: Dynamic, + arg2: Dynamic| + -> Result> { + Python::with_gil(|py| { + let py_arg1 = dynamic_to_python(py, arg1) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg2 = dynamic_to_python(py, arg2) + .map_err(|e| format!("Type conversion error: {}", e))?; + let result = func + .call1(py, (py_arg1, py_arg2)) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + 3 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move |arg1: Dynamic, + arg2: Dynamic, + arg3: Dynamic| + -> Result> { + Python::with_gil(|py| { + let py_arg1 = dynamic_to_python(py, arg1) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg2 = dynamic_to_python(py, arg2) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg3 = dynamic_to_python(py, arg3) + .map_err(|e| format!("Type conversion error: {}", e))?; + let result = func + .call1(py, (py_arg1, py_arg2, py_arg3)) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + 4 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move |arg1: Dynamic, + arg2: Dynamic, + arg3: Dynamic, + arg4: Dynamic| + -> Result> { + Python::with_gil(|py| { + let py_arg1 = dynamic_to_python(py, arg1) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg2 = dynamic_to_python(py, arg2) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg3 = dynamic_to_python(py, arg3) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg4 = dynamic_to_python(py, arg4) + .map_err(|e| format!("Type conversion error: {}", e))?; + let result = func + .call1(py, (py_arg1, py_arg2, py_arg3, py_arg4)) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + 5 => { + self.engine.register_fn(name, { + let func = func.clone_ref(py); + let name = func_name.clone(); + move |arg1: Dynamic, + arg2: Dynamic, + arg3: Dynamic, + arg4: Dynamic, + arg5: Dynamic| + -> Result> { + Python::with_gil(|py| { + let py_arg1 = dynamic_to_python(py, arg1) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg2 = dynamic_to_python(py, arg2) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg3 = dynamic_to_python(py, arg3) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg4 = dynamic_to_python(py, arg4) + .map_err(|e| format!("Type conversion error: {}", e))?; + let py_arg5 = dynamic_to_python(py, arg5) + .map_err(|e| format!("Type conversion error: {}", e))?; + let result = func + .call1(py, (py_arg1, py_arg2, py_arg3, py_arg4, py_arg5)) + .map_err(|e| format!("Python function '{}' error: {}", name, e))?; + python_to_dynamic(py, &result.bind(py)) + .map_err(|e| format!("Type conversion error: {}", e).into()) + }) + } + }); + } + _ => { + return Err(PyRuntimeError::new_err(format!( + "Function '{}' has {} parameters, but only 0-5 parameters are supported", + name, param_count + ))); + } + } + + Ok(()) + } + + /// List registered Python functions + fn list_python_functions(&self) -> Vec { + self.py_functions.keys().cloned().collect() + } } /// A Python module implemented in Rust using PyO3 #[pymodule] fn rhai_python(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add("__version__", "0.1.1")?; Ok(()) }