refactor: repack external bindings into their own crates (#19)

Split the monolithic crate into a Cargo workspace with the Python and Wasm bindings as separate member crates.

- Cargo.toml: declare a `[workspace]` with members `.`/`crates/swactor-python`/`crates/swactor-wasm`, remove the `python` feature and pyo3 dependency, and change root crate-type from `["cdylib","rlib"]` to `["rlib"]`
- crates/swactor-python: new cdylib crate re-exporting the PyO3 bindings (Runtime/RuntimeConfig/RuntimeHandle/Inbox/Ctx/ActorAddress/RuntimeStats), depending on `swactor` + pyo3; pyproject.toml and uv.lock relocated here from the root
- crates/swactor-wasm: new cdylib crate moved from top-level `wasm/`, depending on `swactor` with `no_random` features
- src/actor.rs: widen `Actor::new`, `AnyActor`, `ContextInner`, and `Ctx::raw_inner` to `pub` so the separate binding crates can drive the runtime
- src/lib.rs: delete the in-tree `python` module and `#[pymodule]`, and gate the `no_random` RNG behind `all(feature = "no_random", not(feature = "getrandom"))`
- tools/: relocate package.json/package-lock.json; drop the now-duplicate `wasm/Cargo.lock`

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
zacheryasc 2026-02-07 17:39:02 +00:00
parent a12323c94f
commit 4371642a27
17 changed files with 57 additions and 176 deletions

5
.gitignore vendored
View file

@ -1,6 +1,5 @@
**/target **/target
tools/depgraph/target/ **/node_modules/
node_modules/
.vscode/ .vscode/
.venv **/.venv
__pycache__ __pycache__

15
Cargo.lock generated
View file

@ -532,7 +532,22 @@ dependencies = [
"crossbeam-queue", "crossbeam-queue",
"crossbeam-utils", "crossbeam-utils",
"getrandom", "getrandom",
]
[[package]]
name = "swactor-python"
version = "0.1.0"
dependencies = [
"pyo3", "pyo3",
"swactor",
]
[[package]]
name = "swactor-wasm"
version = "0.1.0"
dependencies = [
"swactor",
"wasm-bindgen",
] ]
[[package]] [[package]]

View file

@ -1,3 +1,7 @@
[workspace]
members = [".", "crates/swactor-python", "crates/swactor-wasm"]
exclude = ["tools/depgraph"]
[package] [package]
name = "swactor" name = "swactor"
version = "0.1.0" version = "0.1.0"
@ -5,19 +9,17 @@ edition = "2024"
autobenches = false autobenches = false
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["rlib"]
[features] [features]
default = ["getrandom"] default = ["getrandom"]
getrandom = ["dep:getrandom"] getrandom = ["dep:getrandom"]
no_random = [] # compile without access to a source of randomness no_random = [] # compile without access to a source of randomness
python = ["dep:pyo3"]
[dependencies] [dependencies]
getrandom = { version = "0.2", optional = true } getrandom = { version = "0.2", optional = true }
crossbeam-queue = "0.3.12" crossbeam-queue = "0.3.12"
crossbeam-utils = "0.8.21" crossbeam-utils = "0.8.21"
pyo3 = { version = "0.23", features = ["extension-module"], optional = true }
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.5", features = ["html_reports"] }

View file

@ -0,0 +1,12 @@
[package]
name = "swactor-python"
version = "0.1.0"
edition = "2024"
[lib]
name = "swactor"
crate-type = ["cdylib"]
[dependencies]
swactor = { path = "../.." }
pyo3 = { version = "0.23", features = ["extension-module"] }

View file

@ -10,12 +10,11 @@ requires-python = ">=3.9"
[dependency-groups] [dependency-groups]
dev = ["jupyter", "ipykernel"] dev = ["jupyter", "ipykernel"]
[tool.maturin]
features = ["python"]
[tool.uv] [tool.uv]
cache-keys = [ cache-keys = [
{ file = "pyproject.toml" }, { file = "pyproject.toml" },
{ file = "Cargo.toml" }, { file = "Cargo.toml" },
{ file = "src/**/*.rs" }, { file = "src/**/*.rs" },
{ file = "../../Cargo.toml" },
{ file = "../../src/**/*.rs" },
] ]

View file

@ -4,10 +4,9 @@ use std::cell::RefCell;
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyModule; use pyo3::types::PyModule;
use crate::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Ctx}; use ::swactor::actor::{Actor, ActorAddress, ActorInterface, AnyActor, Ctx};
use crate::config::{BackoffPolicy, RuntimeConfig}; use ::swactor::config::{BackoffPolicy, RuntimeConfig};
use crate::runtime::{Inbox, Runtime, RuntimeHandle}; use ::swactor::runtime::{Inbox, Runtime, RuntimeHandle};
use crate::Error;
// ─── PyMsg newtype ─────────────────────────────────────────────────────────── // ─── PyMsg newtype ───────────────────────────────────────────────────────────
@ -38,7 +37,7 @@ impl PyMsg {
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
fn to_py_err(e: Error) -> PyErr { fn to_py_err(e: ::swactor::Error) -> PyErr {
pyo3::exceptions::PyRuntimeError::new_err(e.to_string()) pyo3::exceptions::PyRuntimeError::new_err(e.to_string())
} }
@ -570,7 +569,7 @@ fn build_stats(runtime: &Runtime) -> PyRuntimeStats {
// ─── Module registration ───────────────────────────────────────────────────── // ─── Module registration ─────────────────────────────────────────────────────
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyActorAddress>()?; m.add_class::<PyActorAddress>()?;
m.add_class::<PyCtx>()?; m.add_class::<PyCtx>()?;
m.add_class::<PyInbox>()?; m.add_class::<PyInbox>()?;
@ -582,3 +581,8 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyRuntimeStats>()?; m.add_class::<PyRuntimeStats>()?;
Ok(()) Ok(())
} }
#[pymodule]
fn swactor(m: &Bound<'_, PyModule>) -> PyResult<()> {
register(m)
}

View file

@ -1,5 +1,3 @@
[workspace]
[package] [package]
name = "swactor-wasm" name = "swactor-wasm"
version = "0.1.0" version = "0.1.0"
@ -9,5 +7,5 @@ edition = "2024"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
swactor = { path = "..", default-features = false, features = ["no_random"] } swactor = { path = "../..", default-features = false, features = ["no_random"] }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"

View file

@ -26,16 +26,16 @@ impl ActorAddress {
} }
/// The actor process as represented in the Runtime — thin wrapper around user state. /// The actor process as represented in the Runtime — thin wrapper around user state.
pub(crate) struct Actor<A: ActorInterface>(A); pub struct Actor<A: ActorInterface>(A);
impl<A: ActorInterface> Actor<A> { impl<A: ActorInterface> Actor<A> {
pub(crate) fn new(inner: A) -> Self { pub fn new(inner: A) -> Self {
Self(inner) Self(inner)
} }
} }
/// Trait for type-erased actors — single-message handler. /// Trait for type-erased actors — single-message handler.
pub(crate) trait AnyActor: Send { pub trait AnyActor: Send {
fn handle_any(&mut self, ctx: &Ctx, msg: Box<dyn Any + Send>); fn handle_any(&mut self, ctx: &Ctx, msg: Box<dyn Any + Send>);
} }
@ -51,7 +51,7 @@ where
} }
/// Object-safe inner trait for sending type-erased messages. /// Object-safe inner trait for sending type-erased messages.
pub(crate) trait ContextInner { pub trait ContextInner {
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>; fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>; fn spawn_any(&self, addr: ActorAddress, actor: Box<dyn AnyActor>) -> Result<(), Error>;
fn mailbox_waterlevel(&self) -> usize; fn mailbox_waterlevel(&self) -> usize;
@ -71,8 +71,7 @@ impl<'a> Ctx<'a> {
Self { inner, self_addr } Self { inner, self_addr }
} }
#[cfg(feature = "python")] pub fn raw_inner(&self) -> &dyn ContextInner {
pub(crate) fn raw_inner(&self) -> &dyn ContextInner {
self.inner self.inner
} }

View file

@ -12,7 +12,7 @@ use crate::config::RuntimeConfig;
use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId}; use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId};
use crate::stats::WorkerStats; use crate::stats::WorkerStats;
use super::Worker; use crate::worker::Worker;
// ── Actors ───────────────────────────────────────────────────────── // ── Actors ─────────────────────────────────────────────────────────

View file

@ -12,21 +12,12 @@ pub mod stats;
pub mod runtime; pub mod runtime;
#[cfg(feature = "python")]
mod python;
#[cfg(feature = "python")]
#[pyo3::pymodule]
fn swactor(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
python::register(m)
}
#[cfg(feature = "getrandom")] #[cfg(feature = "getrandom")]
pub(crate) fn get_random(buf: &mut [u8]) { pub(crate) fn get_random(buf: &mut [u8]) {
getrandom::getrandom(buf).unwrap() getrandom::getrandom(buf).unwrap()
} }
#[cfg(feature = "no_random")] #[cfg(all(feature = "no_random", not(feature = "getrandom")))]
pub(crate) fn get_random(buf: &mut [u8]) { pub(crate) fn get_random(buf: &mut [u8]) {
use core::sync::atomic::{AtomicUsize, Ordering}; use core::sync::atomic::{AtomicUsize, Ordering};

View file

@ -14,7 +14,7 @@ use crate::Error;
/// A worker owns a set of actors and runs them in a loop. /// A worker owns a set of actors and runs them in a loop.
pub(crate) struct Worker { pub(crate) struct Worker {
id: WorkerId, id: WorkerId,
pool: ActorPool, pub(crate) pool: ActorPool,
transfer_rx: Receiver<Envelope>, transfer_rx: Receiver<Envelope>,
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>, spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
stats: Arc<WorkerStats>, stats: Arc<WorkerStats>,

138
wasm/Cargo.lock generated
View file

@ -1,138 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "swactor"
version = "0.1.0"
dependencies = [
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "swactor-wasm"
version = "0.1.0"
dependencies = [
"swactor",
"wasm-bindgen",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]