Beni
beni gives Rust developers a magnus-like experience for mruby: a Ruby gem manages the mruby build chain, and Rust crates expose a safe, typed API over the archive it builds. Extracted from the kobako project; APIs follow 0.x semver semantics and may still evolve between minor versions.
beni gem ──rake beni:build──▶ archive + sidecar ──▶ beni-sys ──▶ beni crate
Warning
The beni crate does not yet cover the full mruby C API. Anything missing
stays reachable through the unsafe beni::sys escape hatch — issue reports
for the gaps you hit are welcome.
Packages
All four packages release in lockstep under a single version.
| Package | Registry | Role |
|---|---|---|
beni gem |
rubygems.org | Rake tasks + DSL config that download mruby and build the archive |
beni-sys crate |
crates.io | bindgen FFI surface over the mruby C API |
beni crate |
crates.io | safe typed wrapper over beni-sys, aligned with magnus idioms |
beni-macros crate |
crates.io | the beni crate's wrap, TypedData, and InlineStruct macros, used through beni
|
Getting started
Build the mruby archive with the gem
Add beni to your Gemfile and install the task library in your Rakefile:
require "beni/tasks"
Beni::Tasks.newrake beni:buildThis downloads the default mruby release, builds it with mruby's untouched
upstream default config, and stages vendor/mruby/build/host/lib/ with the
archive and its libmruby.flags.mak compile-flags sidecar — everything the
crates need. The archive's own file name follows the toolchain that built it
(libmruby.a, libmruby.lib under MSVC), and the sidecar names it.
To tune the build, declare a config path and generate the seed:
Beni::Tasks.new do
build_config "build_config/mruby.rb"
endrake beni:config writes a self-contained copy of the upstream default
config to that path. The file is yours to edit — add targets, gems, or
defines; beni never rewrites it.
Embed mruby from Rust
Add the beni crate to your Cargo.toml, then point archive discovery at
the vendor tree the gem staged:
BENI_VENDOR_DIR=$PWD/vendor cargo buildA crate ships its Ruby surface as a Gem and installs it during
interpreter setup:
use beni::{method, Error, Gem, Module, Mrb, Value};
fn answer(_mrb: &Mrb, _self: Value) -> i32 {
42
}
struct WidgetGem;
impl Gem for WidgetGem {
fn init(mrb: &Mrb) -> Result<(), Error> {
let widget = mrb.define_class(c"Widget", mrb.class_object())?;
widget.define_method(mrb, c"answer", method!(answer, 0))?;
Ok(())
}
}
fn main() {
let mrb = Mrb::open().expect("mruby interpreter");
mrb.init_gem::<WidgetGem>().expect("Widget surface");
// Widget#answer now returns 42 to any Ruby code the interpreter runs.
}An interpreter is carried between threads but never reached from two at
once. Mrb is Send and not Sync: handing one to another thread
compiles, while sharing it needs a Mutex of your own. A handle stays on
the thread that made it; wrap it in beni::value::Opaque to carry it
along, and unwrap it there with Mrb::get_inner.
Every build needs the archive: with no discovery variable set the build
fails naming the ones it looked for, rather than handing you a crate
whose every call aborts later. Any C API the typed wrapper does not
cover stays reachable through the unsafe beni::sys escape hatch.
Cross-compile for wasm32-wasip1
Declare a wasi target referencing the wasi-sdk toolchain:
Beni::Tasks.new do
build_config "build_config/mruby.rb"
target :host
target :wasi do
toolchain "wasi-sdk"
end
endand append the cross build to the generated config:
MRuby::CrossBuild.new("wasi") do |conf|
conf.toolchain :wasi
endconf.toolchain :wasi resolves to the wasi toolchain file
beni:vendor:setup stages into the mruby tree whenever wasi-sdk is
selected — the cross-compile settings ship with beni and update with it.
After rake beni:build, name the staged archive and the wasi-sdk root
explicitly for the cargo side (a cross-compiled cargo target never reads
the vendor tree on its own):
MRUBY_LIB_DIR=$PWD/vendor/mruby/build/wasi/lib \
WASI_SDK_PATH=$PWD/vendor/wasi-sdk \
cargo build --target wasm32-wasip1Prism Source
An mruby release whose compiler gem parses with Prism keeps Prism in a
git submodule, which the release tarball leaves out. Declare the
prism toolchain with that submodule's commit; the definition alone
selects it, and beni:vendor:setup stages it into the mruby tree:
Beni::Tasks.new do
version "<mruby release>"
toolchain "prism" do
version "<ruby/prism commit>"
sha256 "…"
end
endThe commit is the one the release records for
mrbgems/mruby-compiler/lib/prism.
Toolchain
beni targets plain mruby and is not bound to WebAssembly. rust-toolchain.toml
keeps wasm32-wasip1 only as a build-verification target for downstream wasi
consumers (kobako). For that target two versions move in lockstep, in both this
repo and kobako; host builds are unaffected.
| Version | Lockstep reason |
|---|---|
| Rust channel |
crt1-command.o references __wasi_init_tp from 1.96 on |
| wasi-sdk 33 | its libc.a supplies __wasi_init_tp
|
Development
After checking out the repo, run bin/setup to install dependencies, then
bundle exec rake for the default gate (tests + RuboCop + Steep). The repo
dogfoods its own gem: the Rakefile wires Beni::Tasks with the validation
config build_config/mruby.rb (host + wasi targets), and a repo-local rake
chain verifies the crates compile against a real archive on both the host
target and wasm32-wasip1:
bundle exec rake rust:verify # beni:build + check/test (host) + check (wasm32)Behavior contracts live in SPEC.md — the source of truth the
implementation follows.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/elct9620/beni.
License
The gem and crates are available as open source under the terms of the Apache License 2.0.