Lambda Smalltalk用プラグインの作成方法を解説します。

概要

プラグインは動的ライブラリ(Windowsでは.dll、Linuxでは.so、macOSでは.dylib)で、Lambda Smalltalkの機能を拡張します。最大限の互換性のためにC ABIを使用します。

必須エクスポート

すべてのプラグインは以下の3つの関数をエクスポートする必要があります:

関数シグネチャ説明
lambda_plugin_init() -> *const c_charプラグイン名を返す(null終端)
lambda_plugin_list() -> *const c_char関数リストを返す(二重null終端)
lambda_plugin_call(name, args, argc) -> FfiResult関数呼び出しをディスパッチ

オプション:

関数シグネチャ説明
lambda_plugin_free(ptr) -> ()プラグインが確保したメモリを解放

FFI型

/// 明示的な長さを持つ文字列(null終端より安全)
#[repr(C)]
pub struct FfiString {
    pub ptr: *const c_char,
    pub len: usize,
}

/// データ交換用の値表現
#[repr(C)]
pub enum FfiValue {
    Nil,
    Int(i64),
    Float(f64),
    Bool(bool),
    String(*const c_char),           // null終端
    StringWithLen(FfiString),        // 明示的な長さ(より安全)
    Array(*const FfiValue, usize),   // ポインタ + 長さ
    Dict(*const FfiDictEntry, usize), // ポインタ + 長さ
}

/// 辞書エントリ(キーと値のペア)
#[repr(C)]
pub struct FfiDictEntry {
    pub key: FfiString,
    pub value: FfiValue,
}

/// プラグイン関数からの戻り値
#[repr(C)]
pub struct FfiResult {
    pub status: i32,        // 0 = 成功, それ以外 = エラー
    pub value: FfiValue,    // 結果(status == 0の場合に有効)
    pub error: *const c_char, // エラーメッセージ(status != 0の場合に有効)
}

最小限の例

// my_plugin/src/lib.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// FFI型定義をここに含める(上記からコピーするか共有crateを使用)

static PLUGIN_NAME: &[u8] = b"my_plugin\0";
static FUNC_LIST: &[u8] = b"greet\0add\0\0";  // 二重null終端

#[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"]  # 動的ライブラリに必須

[dependencies]
# 依存関係をここに追加

ビルド

cargo build --release

# 出力場所:
# Windows: target/release/my_plugin.dll
# Linux:   target/release/libmy_plugin.so
# macOS:   target/release/libmy_plugin.dylib

インストール

コンパイル済みプラグインをplugins/ディレクトリに配置:

lambda-st.exe
plugins/
  └── my_plugin.dll  (または .so/.dylib)

Smalltalkからの使用

"プラグインを読み込む"
Plugin load: 'my_plugin'.

"関数を呼び出す"
(Plugin call: 'my_plugin' function: 'greet' args: #('World')) printNl.
"=> 'Hello, World!'"

(Plugin call: 'my_plugin' function: 'add' args: #(10 20)) printNl.
"=> 30"

メモリ管理

単純型

IntFloatBoolNilについてはメモリ管理は不要です。

文字列

2つの選択肢があります:

  1. 静的文字列(固定レスポンスに推奨):
static HELLO: &[u8] = b"Hello\0";
FfiValue::String(HELLO.as_ptr() as *const c_char)
  1. 動的文字列(クリーンアップが必要):
let msg = CString::new(dynamic_content).unwrap();
FfiValue::String(msg.into_raw())  // クリーンアップなしではメモリリーク

クリーンアップの実装

プラグインが動的メモリを確保する場合は、lambda_plugin_freeを実装:

#[no_mangle]
pub extern "C" fn lambda_plugin_free(ptr: *mut std::ffi::c_void) {
    if ptr.is_null() {
        return;
    }
    unsafe {
        // CStringを再構成してドロップ
        drop(CString::from_raw(ptr as *mut c_char));
    }
}

VMはプラグインからデータをコピーした後にこの関数を呼び出します。

複雑なデータの返却

配列

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);  // 解放を防ぐ

    FfiResult {
        status: 0,
        value: FfiValue::Array(ptr, len),
        error: std::ptr::null(),
    }
}

辞書

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(),
    }
}

データベースプラグインの例

完全な例として、PostgreSQLプラグインを参照:

// ハンドルによる接続管理
static mut CONNECTIONS: Option<Mutex<HashMap<i32, Client>>> = None;

fn pg_connect(args: *const FfiValue, argc: usize) -> FfiResult {
    // 引数から接続文字列をパース
    let conn_str = get_string_from_value(&args[0])?;

    // 接続してハンドルを保存
    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])?;

    // クエリを実行して行をDict配列に変換
    let rows = with_connection(handle, |client| {
        client.query(&sql, &[])
    })?;

    // FfiValue::Array of FfiValue::Dict に変換
    // ...
}

デバッグのヒント

  1. シンプルに始める:最初は静的文字列を返し、徐々に複雑さを追加
  2. nullポインタをチェック:入力ポインタを常に検証
  3. エラー結果を使う:説明的なエラーメッセージを返す
  4. 段階的にテスト:関数を追加する前に各関数をテスト

よくある落とし穴

問題解決策
文字列アクセスでクラッシュnullポインタをチェック、UTF-8を検証
メモリリークlambda_plugin_freeを実装
引数の数が違うargsにアクセスする前にargcをチェック
型の不一致FfiValueのバリアントでマッチ
ライブラリが見つからないプラットフォーム固有の命名を確認(Linuxではlibプレフィックス)

プラットフォームノート

Windows

Linux

macOS