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 resulting libmruby.a. Extracted from the
kobako project; APIs follow 0.x semver
semantics and may still evolve between minor versions.
Packages
All three packages release in lockstep under a single version.
| Package | Registry | Role |
|---|---|---|
beni gem |
rubygems.org | Rake tasks + DSL config that download mruby and build libmruby.a |
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 |
Getting started
Build libmruby.a with the gem
Add beni to your Gemfile and install the task library in your Rakefile:
require "beni/tasks"
Beni::Tasks.new
rake beni:build
This downloads the pinned mruby release, builds it with mruby's untouched
upstream default config, and stages vendor/mruby/build/host/lib/ with
libmruby.a and its libmruby.flags.mak compile-flags sidecar — everything
the crates need.
To tune the build, declare a config path and generate the seed:
Beni::Tasks.new do
build_config "build_config/mruby.rb"
end
rake 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 build
A 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.object_class())?;
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.
}
With no archive discovery variable set, a host build compiles in
placeholder mode: cargo check passes, no FFI surface is exported, and
Mrb::open returns an error — so beni is safe to take as a transitive
dependency. 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
end
and append the cross build to the generated config:
MRuby::CrossBuild.new("wasi") do |conf|
conf.toolchain :wasi
end
conf.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-wasip1
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 the Rust channel and the wasi-sdk version
move in lockstep (the wasm32-wasip1 crt1-command.o references
__wasi_init_tp from Rust 1.96 onward; wasi-sdk 33's libc.a supplies that
symbol) — bump the pair together, in both this repo and kobako. Host builds
are unaffected by the pairing.
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 libmruby.a 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.
Releasing
Releases are cut by release-please: merging the release PR tags the
version and publishes the gem and both crates in lockstep through OIDC
trusted publishing. One-time cleanup: after 0.1.0 ships, remove the
release-as line (and the then-stale last-release-sha) from
release-please-config.json — left in place it pins every subsequent
release to 0.1.0.
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.