Wrenlift

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

AttributeWhat 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));
}