From 088b7f48aadde13a95c01e3b96e67e8a08cae19a Mon Sep 17 00:00:00 2001 From: "Arthur G.P. Schuster" Date: Tue, 12 Aug 2025 21:04:21 +0200 Subject: [PATCH] initial commit https://claude.ai/chat/39b41476-837c-4d19-b91f-6b6cd6ce9629 --- Cargo.toml | 15 ++++ README.md | 119 ++++++++++++++++++++++++++++++ example_usage.py | 124 ++++++++++++++++++++++++++++++++ pyproject.toml | 29 ++++++++ python/rhai/__init__.py | 29 ++++++++ src/lib.rs | 156 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 472 insertions(+) create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 example_usage.py create mode 100644 pyproject.toml create mode 100644 python/rhai/__init__.py create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3eef26b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rhai-python" +version = "0.1.0" +edition = "2021" + +[lib] +name = "rhai_python" +crate-type = ["cdylib"] + +[dependencies] +rhai = { version = "1.17", features = ["sync"] } +pyo3 = { version = "0.20", features = ["extension-module"] } + +[build-dependencies] +pyo3-build-config = "0.20" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0481c4 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Rhai Python Bindings - Setup Instructions + +This is a basic implementation of Python bindings for the Rhai scripting language using PyO3. + +## Prerequisites + +1. **Rust** (latest stable version) + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + +2. **Python** 3.8+ with development headers + ```bash + # Ubuntu/Debian + sudo apt install python3-dev + + # macOS (with Homebrew) + brew install python + + # Windows: Install Python from python.org + ``` + +3. **Maturin** (for building Python extensions) + ```bash + pip install maturin + ``` + +## Project Setup + +1. Create the project directory structure: + ```bash + mkdir rhai-python + cd rhai-python + mkdir src python/rhai + ``` + +2. Copy the provided files to their respective locations: + - `Cargo.toml` → project root + - `pyproject.toml` → project root + - `src/lib.rs` → src directory + - `python/rhai/__init__.py` → python/rhai directory + - `example_usage.py` → project root (for testing) + +## Building + +1. **Development build** (recommended for testing): + ```bash + maturin develop + ``` + This builds the extension and installs it in your current Python environment. + +2. **Release build**: + ```bash + maturin build --release + ``` + +3. **Build wheel for distribution**: + ```bash + maturin build --release --out dist/ + ``` + +## Testing + +After building with `maturin develop`, you can test the bindings: + +```bash +python example_usage.py +``` + +Or interactively: +```python +import rhai + +# Quick evaluation +result = rhai.eval("40 + 2") +print(result) # 42 + +# Using the engine +engine = rhai.RhaiEngine() +engine.set_var("x", 10) +result = engine.eval("x * 2") +print(result) # 20 +``` + +## Current Features + +- ✅ Basic script evaluation +- ✅ Variable binding (Python → Rhai) +- ✅ Variable retrieval (Rhai → Python) +- ✅ Type conversion for: + - Integers, floats, booleans, strings + - Arrays (lists) + - Objects (dictionaries) +- ✅ Error handling +- ✅ Script compilation checking +- ✅ Scope management + +## Limitations & TODOs + +This is a basic implementation. Future enhancements could include: + +- [ ] Function registration (calling Python functions from Rhai) +- [ ] Custom type support +- [ ] Module system integration +- [ ] AST manipulation +- [ ] Debugging support +- [ ] Threading/async support +- [ ] Performance optimizations +- [ ] More comprehensive error types + +## Troubleshooting + +**Import errors**: Make sure you've run `maturin develop` after making changes. + +**Build errors**: Ensure you have the correct Python development headers installed. + +**Type conversion errors**: The current implementation supports basic types. Complex Python objects will raise conversion errors. + +**Performance**: This is a proof-of-concept implementation. Production use would benefit from optimization. diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..3f93077 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Example usage of the Rhai Python bindings +""" + +import rhai + + +def main(): + print("=== Basic Rhai Python Bindings Demo ===\n") + + # Quick evaluation + print("1. Quick evaluation:") + result = rhai.eval("40 + 2") + print(f" rhai.eval('40 + 2') = {result}") + print() + + # Using the engine directly + print("2. Using RhaiEngine:") + engine = rhai.RhaiEngine() + + # Basic arithmetic + result = engine.eval("10 * 5 + 3") + print(f" 10 * 5 + 3 = {result}") + + # String operations + result = engine.eval('"Hello, " + "World!"') + print(f" String concatenation: {result}") + + # Boolean operations + result = engine.eval("true && false") + print(f" Boolean operation: {result}") + print() + + # Variables + print("3. Variable operations:") + engine.set_var("x", 42) + engine.set_var("name", "Rhai") + + result = engine.eval("x * 2") + print(f" x = 42, x * 2 = {result}") + + result = engine.eval('"Hello, " + name + "!"') + print(f" name = 'Rhai', greeting = {result}") + + # Check variables + print(f" Variables in scope: {engine.list_vars()}") + print(f" Has variable 'x': {engine.has_var('x')}") + print(f" Get variable 'x': {engine.get_var('x')}") + print() + + # Arrays + print("4. Array operations:") + result = engine.eval("[1, 2, 3, 4, 5]") + print(f" Array creation: {result}") + + engine.set_var("arr", [10, 20, 30]) + result = engine.eval("arr[1]") + print(f" Array indexing arr[1]: {result}") + + result = engine.eval("arr.len()") + print(f" Array length: {result}") + print() + + # Objects/Maps + print("5. Object/Map operations:") + engine.set_var("obj", {"name": "test", "value": 123}) + result = engine.eval("obj.name") + print(f" Object property access: {result}") + + result = engine.eval('#{a: 1, b: "hello", c: true}') + print(f" Object creation: {result}") + print() + + # Control flow + print("6. Control flow:") + result = engine.eval(""" + let x = 10; + if x > 5 { + "x is greater than 5" + } else { + "x is not greater than 5" + } + """) + print(f" If statement result: {result}") + + result = engine.eval(""" + let sum = 0; + for i in 1..6 { + sum += i; + } + sum + """) + print(f" Loop sum 1-5: {result}") + print() + + # Functions + print("7. Functions:") + result = engine.eval(""" + fn double(x) { + x * 2 + } + double(21) + """) + print(f" Custom function result: {result}") + print() + + # Error handling + print("8. Error handling:") + try: + engine.eval("undefined_variable") + except RuntimeError as e: + print(f" Caught error: {e}") + + try: + engine.compile("invalid syntax {{") + except RuntimeError as e: + print(f" Compilation error: {e}") + + print("\n=== Demo Complete ===") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7093f2d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "rhai-python" +description = "Python bindings for the Rhai scripting language" +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] +license = {text = "MIT"} +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", +] +dynamic = ["version"] + +[tool.maturin] +features = ["pyo3/extension-module"] +module-name = "rhai_python" diff --git a/python/rhai/__init__.py b/python/rhai/__init__.py new file mode 100644 index 0000000..c1a9471 --- /dev/null +++ b/python/rhai/__init__.py @@ -0,0 +1,29 @@ +""" +Rhai Python Bindings + +Python bindings for the Rhai scripting language. +""" + +from .rhai_python import RhaiEngine, __version__ + +__all__ = ["RhaiEngine", "__version__"] + + +# Convenience function for quick evaluation +def eval(script: str): + """ + Evaluate a Rhai script and return the result. + + Args: + script: The Rhai script to evaluate + + Returns: + The result of the script evaluation + + Example: + >>> import rhai + >>> rhai.eval("40 + 2") + 42 + """ + engine = RhaiEngine() + return engine.eval(script) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a69fa93 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,156 @@ +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList}; +use rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Scope}; +use std::collections::HashMap; + +/// Convert Rhai Dynamic to Python object +fn dynamic_to_python(py: Python<'_>, value: Dynamic) -> PyResult { + match value.type_name() { + "i64" => Ok(value.as_int().unwrap().to_object(py)), + "f64" => Ok(value.as_float().unwrap().to_object(py)), + "bool" => Ok(value.as_bool().unwrap().to_object(py)), + "string" => Ok(value.into_string().unwrap().to_object(py)), + "char" => Ok(value.as_char().unwrap().to_string().to_object(py)), + "()" => Ok(py.None()), + "array" => { + let array = value.cast::(); + let py_list = PyList::empty(py); + for item in array { + py_list.append(dynamic_to_python(py, item)?)?; + } + Ok(py_list.to_object(py)) + } + "map" => { + let map = value.cast::(); + let py_dict = PyDict::new(py); + for (key, value) in map { + let py_key = key.to_object(py); + let py_value = dynamic_to_python(py, value)?; + py_dict.set_item(py_key, py_value)?; + } + Ok(py_dict.to_object(py)) + } + _ => Err(PyRuntimeError::new_err(format!( + "Unsupported Rhai type: {}", + value.type_name() + ))), + } +} + +/// Convert Python object to Rhai Dynamic +fn python_to_dynamic(py: Python<'_>, obj: &PyAny) -> PyResult { + if obj.is_none() { + Ok(Dynamic::UNIT) + } else if let Ok(val) = obj.extract::() { + Ok(Dynamic::from(val)) + } else if let Ok(val) = obj.extract::() { + Ok(Dynamic::from(val)) + } else if let Ok(val) = obj.extract::() { + Ok(Dynamic::from(val)) + } else if let Ok(val) = obj.extract::() { + Ok(Dynamic::from(val)) + } else if let Ok(py_list) = obj.downcast::() { + let mut array = Array::new(); + for item in py_list { + array.push(python_to_dynamic(py, item)?); + } + Ok(Dynamic::from(array)) + } else if let Ok(py_dict) = obj.downcast::() { + let mut map = Map::new(); + for (key, value) in py_dict { + let key_str = key.extract::()?; + let rhai_value = python_to_dynamic(py, value)?; + map.insert(key_str.into(), rhai_value); + } + Ok(Dynamic::from(map)) + } else { + Err(PyRuntimeError::new_err(format!( + "Unsupported Python type: {}", + obj.get_type().name()? + ))) + } +} + +#[pyclass] +struct RhaiEngine { + engine: Engine, + scope: Scope<'static>, +} + +#[pymethods] +impl RhaiEngine { + #[new] + fn new() -> Self { + Self { + engine: Engine::new(), + scope: Scope::new(), + } + } + + /// Evaluate a Rhai script and return the result + fn eval(&mut self, py: Python<'_>, script: &str) -> PyResult { + match self + .engine + .eval_with_scope::(&mut self.scope, script) + { + Ok(result) => dynamic_to_python(py, result), + Err(err) => Err(PyRuntimeError::new_err(format!("Rhai error: {}", err))), + } + } + + /// Set a variable in the engine scope + fn set_var(&mut self, py: Python<'_>, name: &str, value: &PyAny) -> PyResult<()> { + let dynamic_value = python_to_dynamic(py, value)?; + self.scope.set_value(name, dynamic_value); + Ok(()) + } + + /// Get a variable from the engine scope + fn get_var(&self, py: Python<'_>, name: &str) -> PyResult { + match self.scope.get_value::(name) { + Some(value) => dynamic_to_python(py, value), + None => Err(PyRuntimeError::new_err(format!( + "Variable '{}' not found", + name + ))), + } + } + + /// Check if a variable exists in the scope + fn has_var(&self, name: &str) -> bool { + self.scope.contains(name) + } + + /// Clear all variables from the scope + fn clear_scope(&mut self) { + self.scope.clear(); + } + + /// Compile a script and return whether it's valid + fn compile(&self, script: &str) -> PyResult { + match self.engine.compile(script) { + Ok(_) => Ok(true), + Err(err) => Err(PyRuntimeError::new_err(format!( + "Compilation error: {}", + err + ))), + } + } + + /// Get the list of all variable names in the scope + fn list_vars(&self) -> Vec { + self.scope + .iter() + .map(|(name, _)| name.to_string()) + .collect() + } +} + +/// A Python module implemented in Rust using PyO3 +#[pymodule] +fn rhai_python(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add("__version__", "0.1.0")?; + Ok(()) +}