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"
メモリ管理
単純型
Int、Float、Bool、Nilについてはメモリ管理は不要です。
文字列
2つの選択肢があります:
- 静的文字列(固定レスポンスに推奨):
static HELLO: &[u8] = b"Hello\0";
FfiValue::String(HELLO.as_ptr() as *const c_char)
- 動的文字列(クリーンアップが必要):
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 に変換
// ...
}
デバッグのヒント
- シンプルに始める:最初は静的文字列を返し、徐々に複雑さを追加
- nullポインタをチェック:入力ポインタを常に検証
- エラー結果を使う:説明的なエラーメッセージを返す
- 段階的にテスト:関数を追加する前に各関数をテスト
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| 文字列アクセスでクラッシュ | nullポインタをチェック、UTF-8を検証 |
| メモリリーク | lambda_plugin_freeを実装 |
| 引数の数が違う | argsにアクセスする前にargcをチェック |
| 型の不一致 | FfiValueのバリアントでマッチ |
| ライブラリが見つからない | プラットフォーム固有の命名を確認(Linuxではlibプレフィックス) |
プラットフォームノート
Windows
- 出力:
my_plugin.dll - プレフィックス不要
Linux
- 出力:
libmy_plugin.so - Lambda Smalltalk用に
my_plugin.soにリネーム
macOS
- 出力:
libmy_plugin.dylib - Lambda Smalltalk用に
my_plugin.dylibにリネーム