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:

FunctionSignatureDescription
lambda_plugin_init() -> *const c_charReturns plugin name (null-terminated)
lambda_plugin_list() -> *const c_charReturns function list (double-null terminated)
lambda_plugin_call(name, args, argc) -> FfiResultDispatches function calls

Optional:

FunctionSignatureDescription
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:

  1. Static strings (recommended for fixed responses):
static HELLO: &[u8] = b"Hello\0";
FfiValue::String(HELLO.as_ptr() as *const c_char)
  1. 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

  1. Start simple: Return static strings first, then add complexity
  2. Check null pointers: Always validate input pointers
  3. Use error results: Return descriptive error messages
  4. Test incrementally: Test each function before adding more

Common Pitfalls

ProblemSolution
Crash on string accessCheck for null pointers, validate UTF-8
Memory leakImplement lambda_plugin_free
Wrong argument countCheck argc before accessing args
Type mismatchMatch on FfiValue variants
Library not foundCheck platform-specific naming (lib prefix on Linux)

Platform Notes

Windows

Linux

macOS