Foreign classes
foreign class + the #!native and #!symbol attributes wire a Wren class to a native shared library. Used by every @hatch: plugin; also the way you'd build your own bridge into a Rust crate.
The shape
A foreign class declares the Wren-side surface and points at a native shared library. Method bodies are empty — every call routes to a Rust function in the named cdylib.
#!native = "sqlite"
foreign class Database {
#!symbol = "wlift_sqlite_database_open"
foreign static open(path)
#!symbol = "wlift_sqlite_database_query"
foreign query(sql, params)
}
Wren scripts use it like any other class:
import "@hatch:sqlite" for Database
var db = Database.open(":memory:")
var rows = db.query("SELECT * FROM users WHERE id = ?", [42])
Attributes
| Attribute | What it does |
|---|---|
#!native = "name" | On a foreign class: name of the cdylib (without the lib prefix or extension). The runtime looks for libname.so / libname.dylib / name.dll alongside the bundle. |
#!symbol = "fn_name" | On a foreign method: name of the C-ABI function the cdylib exports. Defaults to the method's mangled name when omitted, but explicit symbols travel better across renames. |
#!wasm = "..." | Same as #!native but only consulted when the runtime is the wasm build. Lets a single bundle target both native and browser by declaring two backing libraries. |
Wasm path
On the wasm runtime, plugins are statically linked into
the host wasm module (browsers can't dlopen).
Build a parallel cdylib targeting wasm32-unknown-unknown
and stage it at libs/<name>_web.wasm; the
hatchfile's web_crate entry tells CI what to
build.
[plugin_source]
repo = "https://github.com/wrenlift/WrenLift.git"
rev = "..."
crate = "wlift_sqlite" # native cdylib
web_crate = "wlift_sqlite_web" # wasm32 companion
ABI handshake
Foreign methods run against the
wlift_abi opaque-VM C shim — a stable C
surface the host process exports. Plugin cdylibs depend
only on wlift_abi (not wren_lift);
host VM layout changes don't break already-built plugins.
Each plugin reports a numeric ABI version through the
runtime's register_plugin registration call;
the host refuses to dlopen mismatches.
A complete example
// counter.wren
#!native = "counter"
foreign class Counter {
#!symbol = "counter_new"
foreign static create(start)
#!symbol = "counter_next"
foreign next
}
var c = Counter.create(10)
System.print(c.next) // 11
System.print(c.next) // 12
// src/lib.rs of the counter cdylib
use wlift_abi::{slot, set_return, Value, WrenVm};
#[no_mangle]
pub unsafe extern "C" fn counter_new(vm: *mut WrenVm) {
let start = slot(vm, 1).as_num().unwrap_or(0.0) as i64;
// ... stash `start` in a registry, get a handle back ...
set_return(vm, Value::num(handle as f64));
}
#[no_mangle]
pub unsafe extern "C" fn counter_next(vm: *mut WrenVm) {
let handle = slot(vm, 0).as_num().unwrap_or(0.0) as u64;
let next = bump(handle);
set_return(vm, Value::num(next as f64));
}