This guide explains how to create plugins for Lambda Smalltalk.
Overview
Plugins are dynamic libraries (.dll on Windows, .so on Linux, .dylib on macOS) that extend Lambda Smalltalk with new functionality. They use a C ABI for maximum compatibility.
Required Exports
Every plugin must export these three functions:
| Function | Signature | Description |
|---|---|---|
lambda_plugin_init | () -> *const c_char | Returns plugin name (null-terminated) |
lambda_plugin_list | () -> *const c_char | Returns function list (double-null terminated) |
lambda_plugin_call | (name, args, argc) -> FfiResult | Dispatches function calls |
Optional:
| Function | Signature | Description |
|---|---|---|
lambda_plugin_free | (ptr) -> () | Frees plugin-allocated memory |
FFI Types
/// String with explicit length (preferred over null-terminated)
#[repr(C)]
pub struct FfiString {
pub ptr: *const c_char,
pub len: usize,
}
/// Value representation for data exchange
#[repr(C)]
pub enum FfiValue {
Nil,
Int(i64),
Float(f64),
Bool(bool),
String(*const c_char), // null-terminated
StringWithLen(FfiString), // explicit length (safer)
Array(*const FfiValue, usize), // pointer + length
Dict(*const FfiDictEntry, usize), // pointer + length
}
/// Dictionary entry (key-value pair)
#[repr(C)]
pub struct FfiDictEntry {
pub key: FfiString,
pub value: FfiValue,
}
/// Result from plugin functions
#[repr(C)]
pub struct FfiResult {
pub status: i32, // 0 = success, non-zero = error
pub value: FfiValue, // result (valid if status == 0)
pub error: *const c_char, // error message (valid if status != 0)
}
Minimal Example
// my_plugin/src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Include FFI type definitions (copy from above or use a shared crate)
static PLUGIN_NAME: &[u8] = b"my_plugin\0";
static FUNC_LIST: &[u8] = b"greet\0add\0\0"; // double-null terminated
#[no_mangle]
pub extern "C" fn lambda_plugin_init() -> *const c_char {
PLUGIN_NAME.as_ptr() as *const c_char
}
#[no_mangle]
pub extern "C" fn lambda_plugin_list() -> *const c_char {
FUNC_LIST.as_ptr() as *const c_char
}
#[no_mangle]
pub extern "C" fn lambda_plugin_call(
name: *const c_char,
args: *const FfiValue,
argc: usize,
) -> FfiResult {
let func_name = unsafe { CStr::from_ptr(name).to_str().unwrap() };
match func_name {
"greet" => my_greet(args, argc),
"add" => my_add(args, argc),
_ => error_result("Unknown function"),
}
}
fn my_greet(args: *const FfiValue, argc: usize) -> FfiResult {
if argc < 1 {
return error_result("greet requires 1 argument");
}
let args = unsafe { std::slice::from_raw_parts(args, argc) };
let name = match &args[0] {
FfiValue::String(p) => unsafe {
CStr::from_ptr(*p).to_str().unwrap_or("stranger")
},
_ => "stranger",
};
let msg = format!("Hello, {}!", name);
let c_str = CString::new(msg).unwrap();
FfiResult {
status: 0,
value: FfiValue::String(c_str.into_raw()),
error: std::ptr::null(),
}
}
fn my_add(args: *const FfiValue, argc: usize) -> FfiResult {
if argc < 2 {
return error_result("add requires 2 arguments");
}
let args = unsafe { std::slice::from_raw_parts(args, argc) };
let (a, b) = match (&args[0], &args[1]) {
(FfiValue::Int(a), FfiValue::Int(b)) => (*a, *b),
_ => return error_result("add requires integers"),
};
FfiResult {
status: 0,
value: FfiValue::Int(a + b),
error: std::ptr::null(),
}
}
fn error_result(msg: &str) -> FfiResult {
let s = CString::new(msg).unwrap();
FfiResult {
status: -1,
value: FfiValue::Nil,
error: s.into_raw(),
}
}
Cargo.toml
[package]
name = "my_plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"] # Required for dynamic library
[dependencies]
# Add your dependencies here
Building
cargo build --release
# Output location:
# Windows: target/release/my_plugin.dll
# Linux: target/release/libmy_plugin.so
# macOS: target/release/libmy_plugin.dylib
Installation
Place the compiled plugin in the plugins/ directory:
lambda-st.exe
plugins/
└── my_plugin.dll (or .so/.dylib)
Usage from Smalltalk
"Load the plugin"
Plugin load: 'my_plugin'.
"Call functions"
(Plugin call: 'my_plugin' function: 'greet' args: #('World')) printNl.
"=> 'Hello, World!'"
(Plugin call: 'my_plugin' function: 'add' args: #(10 20)) printNl.
"=> 30"
Memory Management
Simple Types
For Int, Float, Bool, and Nil, no memory management is needed.
Strings
Two options:
- Static strings (recommended for fixed responses):
static HELLO: &[u8] = b"Hello\0";
FfiValue::String(HELLO.as_ptr() as *const c_char)
- Dynamic strings (requires cleanup):
let msg = CString::new(dynamic_content).unwrap();
FfiValue::String(msg.into_raw()) // Leaks memory without cleanup
Implementing Cleanup
If your plugin allocates dynamic memory, implement lambda_plugin_free:
#[no_mangle]
pub extern "C" fn lambda_plugin_free(ptr: *mut std::ffi::c_void) {
if ptr.is_null() {
return;
}
unsafe {
// Reconstruct and drop the CString
drop(CString::from_raw(ptr as *mut c_char));
}
}
The VM calls this function after copying data from your plugin.
Returning Complex Data
Arrays
fn return_array() -> FfiResult {
let items = vec![
FfiValue::Int(1),
FfiValue::Int(2),
FfiValue::Int(3),
];
let ptr = items.as_ptr();
let len = items.len();
std::mem::forget(items); // Prevent deallocation
FfiResult {
status: 0,
value: FfiValue::Array(ptr, len),
error: std::ptr::null(),
}
}
Dictionaries
fn return_dict() -> FfiResult {
let key = FfiString {
ptr: b"name\0".as_ptr() as *const c_char,
len: 4
};
let value = FfiValue::String(b"Alice\0".as_ptr() as *const c_char);
let entries = vec![FfiDictEntry { key, value }];
let ptr = entries.as_ptr();
let len = entries.len();
std::mem::forget(entries);
FfiResult {
status: 0,
value: FfiValue::Dict(ptr, len),
error: std::ptr::null(),
}
}
Database Plugin Example
For a complete example, see the PostgreSQL plugin:
// Connection management with handles
static mut CONNECTIONS: Option<Mutex<HashMap<i32, Client>>> = None;
fn pg_connect(args: *const FfiValue, argc: usize) -> FfiResult {
// Parse connection string from args
let conn_str = get_string_from_value(&args[0])?;
// Connect and store handle
let client = Client::connect(&conn_str, NoTls)?;
let handle = register_connection(client);
FfiResult {
status: 0,
value: FfiValue::Int(handle as i64),
error: std::ptr::null(),
}
}
fn pg_query(args: *const FfiValue, argc: usize) -> FfiResult {
let handle = get_handle(&args[0])?;
let sql = get_string_from_value(&args[1])?;
// Execute query and convert rows to Array of Dicts
let rows = with_connection(handle, |client| {
client.query(&sql, &[])
})?;
// Convert to FfiValue::Array of FfiValue::Dict
// ...
}
Debugging Tips
- Start simple: Return static strings first, then add complexity
- Check null pointers: Always validate input pointers
- Use error results: Return descriptive error messages
- Test incrementally: Test each function before adding more
Common Pitfalls
| Problem | Solution |
|---|---|
| Crash on string access | Check for null pointers, validate UTF-8 |
| Memory leak | Implement lambda_plugin_free |
| Wrong argument count | Check argc before accessing args |
| Type mismatch | Match on FfiValue variants |
| Library not found | Check platform-specific naming (lib prefix on Linux) |
Platform Notes
Windows
- Output:
my_plugin.dll - No prefix needed
Linux
- Output:
libmy_plugin.so - Rename to
my_plugin.sofor Lambda Smalltalk
macOS
- Output:
libmy_plugin.dylib - Rename to
my_plugin.dylibfor Lambda Smalltalk