diff --git a/.gitignore b/.gitignore index d9f1549..49ddda5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,14 @@ index.d.ts # npm pack tarballs *.tgz +# Python +.venv/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +python/**/*.so + # Compiled binaries guest-examples/hello-c guest-examples/hello-cpp diff --git a/Cargo.lock b/Cargo.lock index 5fd19e5..079cfd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,6 +435,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -442,6 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -450,6 +466,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -468,10 +512,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -560,6 +610,12 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hex" version = "0.4.3" @@ -753,6 +809,8 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "pyo3", + "pyo3-asyncio", "tokio", ] @@ -892,6 +950,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1034,6 +1101,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "metrics" version = "0.24.3" @@ -1409,6 +1485,82 @@ dependencies = [ "syslog", ] +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-asyncio" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea6b68e93db3622f3bb3bf363246cf948ed5375afe7abff98ccbdd50b184995" +dependencies = [ + "futures", + "once_cell", + "pin-project-lite", + "pyo3", + "tokio", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2021,6 +2173,12 @@ dependencies = [ "libc", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "termcolor" version = "1.4.1" @@ -2264,6 +2422,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index cf71639..1df2c3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,14 @@ libc = "0.2.178" napi = { version = "3.5.0", optional = true, features = ["async", "serde-json"] } napi-derive = { version = "3.3.0", optional = true } +# Python bindings (optional) +pyo3 = { version = "0.20", optional = true, features = ["extension-module", "abi3-py38"] } +pyo3-asyncio = { version = "0.20", optional = true, features = ["tokio-runtime"] } + [features] default = [] napi = ["dep:napi", "dep:napi-derive", "dep:napi-build"] +python = ["dep:pyo3", "dep:pyo3-asyncio"] [lib] crate-type = ["cdylib", "rlib"] diff --git a/README.md b/README.md index 84e3fe8..998a9ec 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,29 @@ npm run build node examples/napi.js ``` +### Python + + +> **Setup (one-time):** +> ```bash +> pipx install maturin # to install, see: https://pipx.pypa.io/stable/ +> +> pipx ensurepath # adds ~/.local/bin to PATH +> +> # Restart your terminal or run: export PATH="$HOME/.local/bin:$PATH" +> ``` + +Run from Python: +```bash +# Create and activate venv +python3 -m venv .venv +source .venv/bin/activate + +# Build the Rust extension and install it in venv then run example +maturin develop --features python +python examples/python_sdk_example.py +``` + ### C & C++ Programs For compiled languages, you'll need to compile first, then run. @@ -120,6 +143,26 @@ if (result.success) { } ``` +### Python + +```python +import asyncio +from hyperlight_nanvix import NanvixSandbox, SandboxConfig + +async def main(): + config = SandboxConfig( + log_directory="/tmp/hyperlight-nanvix", + tmp_directory="/tmp/hyperlight-nanvix" + ) + sandbox = NanvixSandbox(config) + + result = await sandbox.run('guest-examples/hello.js') + if result.success: + print('Execution completed') + +asyncio.run(main()) +``` + To embed in your own project: ```bash diff --git a/examples/python_sdk_example.py b/examples/python_sdk_example.py new file mode 100644 index 0000000..021be19 --- /dev/null +++ b/examples/python_sdk_example.py @@ -0,0 +1,20 @@ +import asyncio +from hyperlight_nanvix import NanvixSandbox + +async def main(): + print("Running guest-examples/hello.js...") + + try: + sandbox = NanvixSandbox() + result = await sandbox.run("guest-examples/hello.js") + + if result.success: + print("Workload completed successfully!") + else: + print(f"Error: {result.error}") + exit(1) + except Exception as error: + print(f"Error: {error}") + exit(1) + +asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fee03ef --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "hyperlight-nanvix" +version = "0.1.0" +requires-python = ">=3.8" + +[tool.maturin] +features = ["python"] +module-name = "hyperlight_nanvix.hyperlight_nanvix" +python-source = "python" diff --git a/python/hyperlight_nanvix/__init__.py b/python/hyperlight_nanvix/__init__.py new file mode 100644 index 0000000..ae327a1 --- /dev/null +++ b/python/hyperlight_nanvix/__init__.py @@ -0,0 +1,12 @@ +""" +hyperlight-nanvix: Python bindings for running sandboxed workloads + +This module provides a Python interface to the Nanvix microkernel-based +sandbox system, allowing you to run JavaScript, Python, C, and C++ +workloads in isolated environments. +""" + +from .hyperlight_nanvix import NanvixSandbox, SandboxConfig, WorkloadResult + +__version__ = "0.1.0" +__all__ = ["NanvixSandbox", "SandboxConfig", "WorkloadResult"] diff --git a/python/hyperlight_nanvix/__init__.pyi b/python/hyperlight_nanvix/__init__.pyi new file mode 100644 index 0000000..a1e30a4 --- /dev/null +++ b/python/hyperlight_nanvix/__init__.pyi @@ -0,0 +1,17 @@ +from typing import Optional + +class SandboxConfig: + log_directory: Optional[str] + tmp_directory: Optional[str] + def __init__(self, log_directory: Optional[str] = None, tmp_directory: Optional[str] = None) -> None: ... + +class WorkloadResult: + success: bool + error: Optional[str] + +class NanvixSandbox: + def __init__(self, config: Optional[SandboxConfig] = None) -> None: ... + async def run(self, workload_path: str) -> WorkloadResult: ... + async def clear_cache(self) -> bool: ... + +__all__ = ["NanvixSandbox", "SandboxConfig", "WorkloadResult"] diff --git a/src/lib.rs b/src/lib.rs index b900744..443b19b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,9 @@ pub mod runtime; #[cfg(feature = "napi")] pub mod napi; +#[cfg(feature = "python")] +pub mod python; + #[cfg(test)] mod unit_tests; diff --git a/src/python.rs b/src/python.rs new file mode 100644 index 0000000..ac7c339 --- /dev/null +++ b/src/python.rs @@ -0,0 +1,150 @@ +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use std::sync::Arc; + +use crate::runtime::{Runtime, RuntimeConfig}; + +/// Python wrapper for hyperlight-nanvix Runtime +#[pyclass] +pub struct NanvixSandbox { + runtime: Arc, +} + +/// Configuration options for creating a sandbox +#[pyclass] +#[derive(Clone)] +pub struct SandboxConfig { + #[pyo3(get, set)] + pub log_directory: Option, + #[pyo3(get, set)] + pub tmp_directory: Option, +} + +#[pymethods] +impl SandboxConfig { + #[new] + #[pyo3(signature = (log_directory=None, tmp_directory=None))] + fn new(log_directory: Option, tmp_directory: Option) -> Self { + Self { + log_directory, + tmp_directory, + } + } +} + +/// Workload execution result +#[pyclass] +#[derive(Clone)] +pub struct WorkloadResult { + #[pyo3(get)] + pub success: bool, + #[pyo3(get)] + pub error: Option, +} + +#[pymethods] +impl WorkloadResult { + fn __repr__(&self) -> String { + match &self.error { + Some(err) => format!("WorkloadResult(success={}, error='{}')", self.success, err), + None => format!("WorkloadResult(success={})", self.success), + } + } +} + +#[pymethods] +impl NanvixSandbox { + /// Create a new sandbox instance + /// + /// Args: + /// config: Optional SandboxConfig with log_directory and tmp_directory + /// + /// Returns: + /// A new NanvixSandbox instance + /// + /// Example: + /// >>> sandbox = NanvixSandbox() + /// >>> config = SandboxConfig(log_directory="/tmp/logs") + /// >>> sandbox = NanvixSandbox(config) + #[new] + #[pyo3(signature = (config=None))] + fn new(config: Option) -> PyResult { + let runtime_config = match config { + Some(cfg) => { + let mut runtime_config = RuntimeConfig::new(); + if let Some(log_dir) = cfg.log_directory { + runtime_config = runtime_config.with_log_directory(log_dir); + } + if let Some(tmp_dir) = cfg.tmp_directory { + runtime_config = runtime_config.with_tmp_directory(tmp_dir); + } + runtime_config + } + None => RuntimeConfig::new(), + }; + + let runtime = Runtime::new(runtime_config) + .map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {}", e)))?; + + Ok(Self { runtime: Arc::new(runtime) }) + } + + /// Run a workload in the sandbox + /// + /// Args: + /// workload_path: Path to the workload file (JavaScript, Python, or binary) + /// + /// Returns: + /// WorkloadResult indicating success or failure + /// + /// Example: + /// >>> result = await sandbox.run("script.py") + /// >>> if result.success: + /// ... print("Success!") + fn run<'py>(&self, py: Python<'py>, workload_path: String) -> PyResult<&'py PyAny> { + let runtime = Arc::clone(&self.runtime); + + pyo3_asyncio::tokio::future_into_py(py, async move { + match runtime.run(&workload_path).await { + Ok(()) => Ok(WorkloadResult { + success: true, + error: None, + }), + Err(e) => Ok(WorkloadResult { + success: false, + error: Some(format!("Workload execution failed: {}", e)), + }), + } + }) + } + + /// Clear the binary cache + /// + /// Returns: + /// True if cache was cleared successfully + /// + /// Example: + /// >>> success = await sandbox.clear_cache() + fn clear_cache<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> { + let runtime = Arc::clone(&self.runtime); + + pyo3_asyncio::tokio::future_into_py(py, async move { + runtime.clear_cache().await + .map_err(|e| PyRuntimeError::new_err(format!("Failed to clear cache: {}", e)))?; + Ok(true) + }) + } + + fn __repr__(&self) -> String { + "NanvixSandbox()".to_string() + } +} + +/// Initialize the Python module +#[pymodule] +fn hyperlight_nanvix(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +}