use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{CString, c_char};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::ffi_standard::{FfiBorrowedBuffer, FfiOwnedBuffer};
use crate::runtime_context::RuntimeRequestContext;
use crate::runtime_help::{RuntimeHelpDetail, RuntimeSkillHelpDescriptor};
use crate::runtime_options::{LuaInvocationContext, RuntimeSkillRoot};
use crate::{
LuaEngine, LuaEngineOptions, RuntimeEntryDescriptor, RuntimeInvocationResult, SkillApplyResult,
SkillInstallRequest, SkillUninstallOptions, SkillUninstallResult,
};
pub(crate) const FFI_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Serialize)]
struct FfiJsonEnvelope<T: Serialize> {
ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
pub(crate) struct FfiEngineSlot {
pub(crate) engine: Arc<Mutex<LuaEngine>>,
}
impl FfiEngineSlot {
pub(crate) fn new(engine: LuaEngine) -> Self {
Self {
engine: Arc::new(Mutex::new(engine)),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct EngineNewJsonRequest {
options: LuaEngineOptions,
}
#[derive(Debug, Serialize, Deserialize)]
struct EngineHandleJsonResult {
engine_id: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct EngineIdJsonRequest {
engine_id: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct EngineRootsJsonRequest {
engine_id: u64,
skill_roots: Vec<RuntimeSkillRoot>,
}
#[derive(Debug, Serialize, Deserialize)]
struct EngineDirsJsonRequest {
engine_id: u64,
base_dir: PathBuf,
#[serde(default)]
override_dir: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DisableSkillDirsJsonRequest {
engine_id: u64,
base_dir: PathBuf,
#[serde(default)]
override_dir: Option<PathBuf>,
skill_id: String,
#[serde(default)]
reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct RenderHelpJsonRequest {
engine_id: u64,
skill_id: String,
flow_name: String,
#[serde(default)]
request_context: Option<RuntimeRequestContext>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PromptCompletionJsonRequest {
engine_id: u64,
prompt_name: String,
argument_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct IsSkillJsonRequest {
engine_id: u64,
tool_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct BoolJsonResult {
value: bool,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillNameForToolJsonRequest {
engine_id: u64,
tool_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct OptionalSkillNameJsonResult {
#[serde(skip_serializing_if = "Option::is_none")]
skill_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillConfigListJsonRequest {
engine_id: u64,
#[serde(default)]
skill_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillConfigGetJsonRequest {
engine_id: u64,
skill_id: String,
key: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillConfigSetJsonRequest {
engine_id: u64,
skill_id: String,
key: String,
value: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillConfigGetJsonResult {
found: bool,
skill_id: String,
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SkillConfigMutationJsonResult {
action: String,
skill_id: String,
key: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
struct CallSkillJsonRequest {
engine_id: u64,
tool_name: String,
#[serde(default = "default_json_object")]
args: Value,
#[serde(default)]
invocation_context: Option<LuaInvocationContext>,
}
#[derive(Debug, Serialize, Deserialize)]
struct RunLuaJsonRequest {
engine_id: u64,
code: String,
#[serde(default = "default_json_object")]
args: Value,
#[serde(default)]
invocation_context: Option<LuaInvocationContext>,
}
#[derive(Debug, Serialize, Deserialize)]
struct DisableSkillJsonRequest {
engine_id: u64,
skill_roots: Vec<RuntimeSkillRoot>,
skill_id: String,
#[serde(default)]
reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct EnableSkillJsonRequest {
engine_id: u64,
skill_roots: Vec<RuntimeSkillRoot>,
skill_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct UninstallSkillJsonRequest {
engine_id: u64,
skill_roots: Vec<RuntimeSkillRoot>,
skill_id: String,
#[serde(default)]
options: SkillUninstallOptions,
}
#[derive(Debug, Serialize, Deserialize)]
struct ApplySkillJsonRequest {
engine_id: u64,
skill_roots: Vec<RuntimeSkillRoot>,
request: SkillInstallRequest,
}
#[derive(Debug, Serialize, Deserialize)]
struct FfiDescribeJsonResult {
ffi_version: String,
exported_functions: Vec<String>,
}
pub(crate) static FFI_ENGINE_REGISTRY: OnceLock<Mutex<HashMap<u64, FfiEngineSlot>>> =
OnceLock::new();
pub(crate) static FFI_ENGINE_COUNTER: AtomicU64 = AtomicU64::new(1);
thread_local! {
static ACTIVE_FFI_ENGINE_IDS: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
}
struct ActiveFfiEngineGuard {
engine_id: u64,
}
impl ActiveFfiEngineGuard {
fn enter(engine_id: u64) -> Result<Self, String> {
ACTIVE_FFI_ENGINE_IDS.with(|active_ids| {
let mut active_ids = active_ids.borrow_mut();
if active_ids.contains(&engine_id) {
return Err(format!(
"FFI engine {} reentrant access is not allowed on the same thread",
engine_id
));
}
active_ids.push(engine_id);
Ok(Self { engine_id })
})
}
}
impl Drop for ActiveFfiEngineGuard {
fn drop(&mut self) {
ACTIVE_FFI_ENGINE_IDS.with(|active_ids| {
let mut active_ids = active_ids.borrow_mut();
if let Some(position) = active_ids
.iter()
.rposition(|active| *active == self.engine_id)
{
active_ids.remove(position);
}
});
}
}
fn default_json_object() -> Value {
Value::Object(serde_json::Map::new())
}
pub(crate) fn ffi_engine_registry() -> &'static Mutex<HashMap<u64, FfiEngineSlot>> {
FFI_ENGINE_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
fn clone_engine_handle(engine_id: u64) -> Result<Arc<Mutex<LuaEngine>>, String> {
let registry = ffi_engine_registry()
.lock()
.map_err(|_| "FFI engine registry lock poisoned".to_string())?;
registry
.get(&engine_id)
.map(|slot| Arc::clone(&slot.engine))
.ok_or_else(|| format!("FFI engine {} not found", engine_id))
}
fn owned_buffer_from_bytes(bytes: &[u8]) -> FfiOwnedBuffer {
if bytes.is_empty() {
return FfiOwnedBuffer {
ptr: std::ptr::null_mut(),
len: 0,
};
}
let mut owned = bytes.to_vec();
let pointer = owned.as_mut_ptr();
let len = owned.len();
std::mem::forget(owned);
FfiOwnedBuffer { ptr: pointer, len }
}
fn encode_json_buffer<T: Serialize>(value: &T) -> FfiOwnedBuffer {
let json_text = serde_json::to_string(value).unwrap_or_else(|error| {
format!(
"{{\"ok\":false,\"error\":\"Failed to serialize FFI response: {}\"}}",
error
)
});
owned_buffer_from_bytes(json_text.as_bytes())
}
fn ffi_ok<T: Serialize>(result: T) -> FfiOwnedBuffer {
encode_json_buffer(&FfiJsonEnvelope {
ok: true,
result: Some(result),
error: None::<String>,
})
}
fn ffi_error(message: impl Into<String>) -> FfiOwnedBuffer {
encode_json_buffer(&FfiJsonEnvelope::<Value> {
ok: false,
result: None,
error: Some(message.into()),
})
}
fn decode_json_request<T: DeserializeOwned>(
input_json: FfiBorrowedBuffer,
function_name: &str,
) -> Result<T, String> {
if input_json.ptr.is_null() {
if input_json.len == 0 {
return Err(format!(
"{} requires one non-null JSON buffer",
function_name
));
}
return Err(format!(
"{} received null JSON buffer with non-zero len",
function_name
));
}
let bytes = unsafe { std::slice::from_raw_parts(input_json.ptr, input_json.len) };
let text = std::str::from_utf8(bytes)
.map_err(|error| format!("{} received invalid UTF-8 input: {}", function_name, error))?;
serde_json::from_str(text)
.map_err(|error| format!("{} received invalid JSON input: {}", function_name, error))
}
pub(crate) fn with_engine<T, F>(engine_id: u64, operation: F) -> Result<T, String>
where
F: FnOnce(&LuaEngine) -> Result<T, String>,
{
let engine_handle = clone_engine_handle(engine_id)?;
let _active_guard = ActiveFfiEngineGuard::enter(engine_id)?;
let engine = engine_handle
.lock()
.map_err(|_| format!("FFI engine {} lock poisoned", engine_id))?;
operation(&engine)
}
pub(crate) fn with_engine_mut<T, F>(engine_id: u64, operation: F) -> Result<T, String>
where
F: FnOnce(&mut LuaEngine) -> Result<T, String>,
{
let engine_handle = clone_engine_handle(engine_id)?;
let _active_guard = ActiveFfiEngineGuard::enter(engine_id)?;
let mut engine = engine_handle
.lock()
.map_err(|_| format!("FFI engine {} lock poisoned", engine_id))?;
operation(&mut engine)
}
pub(crate) fn exported_ffi_function_names() -> Vec<String> {
vec![
"luaskills_ffi_version",
"luaskills_ffi_describe",
"luaskills_ffi_engine_new",
"luaskills_ffi_engine_free",
"luaskills_ffi_load_from_dirs",
"luaskills_ffi_load_from_roots",
"luaskills_ffi_reload_from_dirs",
"luaskills_ffi_reload_from_roots",
"luaskills_ffi_list_entries",
"luaskills_ffi_list_skill_help",
"luaskills_ffi_render_skill_help_detail",
"luaskills_ffi_prompt_argument_completions",
"luaskills_ffi_is_skill",
"luaskills_ffi_skill_name_for_tool",
"luaskills_ffi_skill_config_list",
"luaskills_ffi_skill_config_get",
"luaskills_ffi_skill_config_set",
"luaskills_ffi_skill_config_delete",
"luaskills_ffi_call_skill",
"luaskills_ffi_run_lua",
"luaskills_ffi_disable_skill_in_dirs",
"luaskills_ffi_disable_skill",
"luaskills_ffi_system_disable_skill_in_dirs",
"luaskills_ffi_system_disable_skill",
"luaskills_ffi_enable_skill",
"luaskills_ffi_system_enable_skill",
"luaskills_ffi_uninstall_skill",
"luaskills_ffi_system_uninstall_skill",
"luaskills_ffi_install_skill",
"luaskills_ffi_system_install_skill",
"luaskills_ffi_update_skill",
"luaskills_ffi_system_update_skill",
"luaskills_ffi_set_sqlite_provider_callback",
"luaskills_ffi_set_lancedb_provider_callback",
"luaskills_ffi_set_sqlite_provider_json_callback",
"luaskills_ffi_set_lancedb_provider_json_callback",
"luaskills_ffi_string_clone",
"luaskills_ffi_version_json",
"luaskills_ffi_describe_json",
"luaskills_ffi_engine_new_json",
"luaskills_ffi_engine_free_json",
"luaskills_ffi_load_from_dirs_json",
"luaskills_ffi_load_from_roots_json",
"luaskills_ffi_reload_from_dirs_json",
"luaskills_ffi_reload_from_roots_json",
"luaskills_ffi_list_entries_json",
"luaskills_ffi_list_skill_help_json",
"luaskills_ffi_render_skill_help_detail_json",
"luaskills_ffi_prompt_argument_completions_json",
"luaskills_ffi_is_skill_json",
"luaskills_ffi_skill_name_for_tool_json",
"luaskills_ffi_skill_config_list_json",
"luaskills_ffi_skill_config_get_json",
"luaskills_ffi_skill_config_set_json",
"luaskills_ffi_skill_config_delete_json",
"luaskills_ffi_call_skill_json",
"luaskills_ffi_run_lua_json",
"luaskills_ffi_disable_skill_in_dirs_json",
"luaskills_ffi_disable_skill_json",
"luaskills_ffi_system_disable_skill_in_dirs_json",
"luaskills_ffi_system_disable_skill_json",
"luaskills_ffi_enable_skill_json",
"luaskills_ffi_system_enable_skill_json",
"luaskills_ffi_uninstall_skill_json",
"luaskills_ffi_system_uninstall_skill_json",
"luaskills_ffi_install_skill_json",
"luaskills_ffi_system_install_skill_json",
"luaskills_ffi_update_skill_json",
"luaskills_ffi_system_update_skill_json",
"luaskills_ffi_string_free",
"luaskills_ffi_bytes_clone",
"luaskills_ffi_bytes_free",
"luaskills_ffi_buffer_clone",
"luaskills_ffi_buffer_free",
"luaskills_ffi_string_array_free",
"luaskills_ffi_entry_list_free",
"luaskills_ffi_help_list_free",
"luaskills_ffi_help_detail_free",
"luaskills_ffi_invocation_result_free",
"luaskills_ffi_skill_apply_result_free",
"luaskills_ffi_skill_uninstall_result_free",
]
.into_iter()
.map(str::to_string)
.collect()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_string_free(value: *mut c_char) {
if !value.is_null() {
let _ = unsafe { CString::from_raw(value) };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn luaskills_ffi_version_json() -> FfiOwnedBuffer {
ffi_ok(json!({
"ffi_version": FFI_VERSION,
"protocol": "json-cabi"
}))
}
#[unsafe(no_mangle)]
pub extern "C" fn luaskills_ffi_describe_json() -> FfiOwnedBuffer {
ffi_ok(FfiDescribeJsonResult {
ffi_version: FFI_VERSION.to_string(),
exported_functions: exported_ffi_function_names(),
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_engine_new_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineNewJsonRequest>(
input_json,
"luaskills_ffi_engine_new_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match LuaEngine::new(request.options) {
Ok(engine) => {
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut registry = match ffi_engine_registry().lock() {
Ok(registry) => registry,
Err(_) => return ffi_error("FFI engine registry lock poisoned"),
};
registry.insert(engine_id, FfiEngineSlot::new(engine));
ffi_ok(EngineHandleJsonResult { engine_id })
}
Err(error) => ffi_error(error.to_string()),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_engine_free_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineIdJsonRequest>(
input_json,
"luaskills_ffi_engine_free_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
let mut registry = match ffi_engine_registry().lock() {
Ok(registry) => registry,
Err(_) => return ffi_error("FFI engine registry lock poisoned"),
};
if registry.remove(&request.engine_id).is_none() {
return ffi_error(format!("FFI engine {} not found", request.engine_id));
}
ffi_ok(json!({ "freed": true }))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_load_from_dirs_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineDirsJsonRequest>(
input_json,
"luaskills_ffi_load_from_dirs_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.load_from_dirs(&request.base_dir, request.override_dir.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "loaded": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_load_from_roots_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineRootsJsonRequest>(
input_json,
"luaskills_ffi_load_from_roots_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.load_from_roots(&request.skill_roots)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "loaded": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_reload_from_dirs_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineDirsJsonRequest>(
input_json,
"luaskills_ffi_reload_from_dirs_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.reload_from_dirs(&request.base_dir, request.override_dir.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "reloaded": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_reload_from_roots_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineRootsJsonRequest>(
input_json,
"luaskills_ffi_reload_from_roots_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.reload_from_roots(&request.skill_roots)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "reloaded": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_list_entries_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineIdJsonRequest>(
input_json,
"luaskills_ffi_list_entries_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| Ok(engine.list_entries())) {
Ok(result) => ffi_ok::<Vec<RuntimeEntryDescriptor>>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_list_skill_help_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EngineIdJsonRequest>(
input_json,
"luaskills_ffi_list_skill_help_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| Ok(engine.list_skill_help())) {
Ok(result) => ffi_ok::<Vec<RuntimeSkillHelpDescriptor>>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_render_skill_help_detail_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<RenderHelpJsonRequest>(
input_json,
"luaskills_ffi_render_skill_help_detail_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
engine.render_skill_help_detail(
&request.skill_id,
&request.flow_name,
request.request_context.as_ref(),
)
}) {
Ok(result) => ffi_ok::<Option<RuntimeHelpDetail>>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_prompt_argument_completions_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<PromptCompletionJsonRequest>(
input_json,
"luaskills_ffi_prompt_argument_completions_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
Ok(engine.prompt_argument_completions(&request.prompt_name, &request.argument_name))
}) {
Ok(result) => ffi_ok::<Option<Vec<String>>>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_is_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<IsSkillJsonRequest>(
input_json,
"luaskills_ffi_is_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
Ok(engine.is_skill(&request.tool_name))
}) {
Ok(value) => ffi_ok(BoolJsonResult { value }),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_name_for_tool_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<SkillNameForToolJsonRequest>(
input_json,
"luaskills_ffi_skill_name_for_tool_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
Ok(engine.skill_name_for_tool(&request.tool_name))
}) {
Ok(skill_id) => ffi_ok(OptionalSkillNameJsonResult { skill_id }),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_list_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<SkillConfigListJsonRequest>(
input_json,
"luaskills_ffi_skill_config_list_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
engine.list_skill_config_entries(request.skill_id.as_deref())
}) {
Ok(entries) => ffi_ok(entries),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_get_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<SkillConfigGetJsonRequest>(
input_json,
"luaskills_ffi_skill_config_get_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
engine.get_skill_config_value(&request.skill_id, &request.key)
}) {
Ok(value) => ffi_ok(SkillConfigGetJsonResult {
found: value.is_some(),
skill_id: request.skill_id,
key: request.key,
value,
}),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_set_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<SkillConfigSetJsonRequest>(
input_json,
"luaskills_ffi_skill_config_set_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine.set_skill_config_value(&request.skill_id, &request.key, &request.value)
}) {
Ok(()) => ffi_ok(SkillConfigMutationJsonResult {
action: "set".to_string(),
skill_id: request.skill_id,
key: request.key,
value: Some(request.value),
deleted: None,
}),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_delete_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<SkillConfigGetJsonRequest>(
input_json,
"luaskills_ffi_skill_config_delete_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine.delete_skill_config_value(&request.skill_id, &request.key)
}) {
Ok(deleted) => ffi_ok(SkillConfigMutationJsonResult {
action: "delete".to_string(),
skill_id: request.skill_id,
key: request.key,
value: None,
deleted: Some(deleted),
}),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_call_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<CallSkillJsonRequest>(
input_json,
"luaskills_ffi_call_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
engine.call_skill(
&request.tool_name,
&request.args,
request.invocation_context.as_ref(),
)
}) {
Ok(result) => ffi_ok::<RuntimeInvocationResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_run_lua_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request =
match decode_json_request::<RunLuaJsonRequest>(input_json, "luaskills_ffi_run_lua_json") {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine(request.engine_id, |engine| {
engine.run_lua(
&request.code,
&request.args,
request.invocation_context.as_ref(),
)
}) {
Ok(result) => ffi_ok::<Value>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_disable_skill_in_dirs_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<DisableSkillDirsJsonRequest>(
input_json,
"luaskills_ffi_disable_skill_in_dirs_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.disable_skill(
&request.base_dir,
request.override_dir.as_deref(),
&request.skill_id,
request.reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "disabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_disable_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<DisableSkillJsonRequest>(
input_json,
"luaskills_ffi_disable_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.disable_skill_in_roots(
&request.skill_roots,
&request.skill_id,
request.reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "disabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_disable_skill_in_dirs_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<DisableSkillDirsJsonRequest>(
input_json,
"luaskills_ffi_system_disable_skill_in_dirs_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_disable_skill(
&request.base_dir,
request.override_dir.as_deref(),
&request.skill_id,
request.reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "disabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_disable_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<DisableSkillJsonRequest>(
input_json,
"luaskills_ffi_system_disable_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_disable_skill_in_roots(
&request.skill_roots,
&request.skill_id,
request.reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "disabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_enable_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EnableSkillJsonRequest>(
input_json,
"luaskills_ffi_enable_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.enable_skill(&request.skill_roots, &request.skill_id)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "enabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_enable_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<EnableSkillJsonRequest>(
input_json,
"luaskills_ffi_system_enable_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_enable_skill(&request.skill_roots, &request.skill_id)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok(json!({ "enabled": true })),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_uninstall_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<UninstallSkillJsonRequest>(
input_json,
"luaskills_ffi_uninstall_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.uninstall_skill(&request.skill_roots, &request.skill_id, &request.options)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillUninstallResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_uninstall_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<UninstallSkillJsonRequest>(
input_json,
"luaskills_ffi_system_uninstall_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_uninstall_skill(&request.skill_roots, &request.skill_id, &request.options)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillUninstallResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_install_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<ApplySkillJsonRequest>(
input_json,
"luaskills_ffi_install_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.install_skill(&request.skill_roots, &request.request)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillApplyResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_install_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<ApplySkillJsonRequest>(
input_json,
"luaskills_ffi_system_install_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_install_skill(&request.skill_roots, &request.request)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillApplyResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_update_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<ApplySkillJsonRequest>(
input_json,
"luaskills_ffi_update_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.update_skill(&request.skill_roots, &request.request)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillApplyResult>(result),
Err(error) => ffi_error(error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_update_skill_json(
input_json: FfiBorrowedBuffer,
) -> FfiOwnedBuffer {
let request = match decode_json_request::<ApplySkillJsonRequest>(
input_json,
"luaskills_ffi_system_update_skill_json",
) {
Ok(request) => request,
Err(error) => return ffi_error(error),
};
match with_engine_mut(request.engine_id, |engine| {
engine
.system_update_skill(&request.skill_roots, &request.request)
.map_err(|error| error.to_string())
}) {
Ok(result) => ffi_ok::<SkillApplyResult>(result),
Err(error) => ffi_error(error),
}
}
#[cfg(test)]
mod tests {
use super::{
EngineHandleJsonResult, EngineIdJsonRequest, EngineNewJsonRequest, FFI_ENGINE_COUNTER,
FfiEngineSlot, SkillConfigGetJsonRequest, SkillConfigListJsonRequest,
SkillConfigSetJsonRequest, ffi_engine_registry, luaskills_ffi_engine_free_json,
luaskills_ffi_engine_new_json, luaskills_ffi_skill_config_delete_json,
luaskills_ffi_skill_config_get_json, luaskills_ffi_skill_config_list_json,
luaskills_ffi_skill_config_set_json, with_engine,
};
use crate::ffi_standard::{FfiBorrowedBuffer, FfiOwnedBuffer, luaskills_ffi_buffer_free};
use crate::{LuaEngine, LuaEngineOptions, LuaVmPoolConfig};
use std::ffi::CString;
use std::sync::atomic::Ordering;
use std::sync::{Mutex, MutexGuard, OnceLock};
unsafe fn decode_response_json(buffer: FfiOwnedBuffer) -> serde_json::Value {
let bytes = if buffer.ptr.is_null() {
assert_eq!(buffer.len, 0, "null response pointer must have zero len");
&[][..]
} else {
unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) }
};
let text = std::str::from_utf8(bytes).expect("ffi json must be utf-8");
let value = serde_json::from_str(text).expect("ffi json must parse");
unsafe { luaskills_ffi_buffer_free(buffer) };
value
}
fn borrowed_json_buffer(value: &CString) -> FfiBorrowedBuffer {
let bytes = value.as_bytes();
FfiBorrowedBuffer {
ptr: bytes.as_ptr(),
len: bytes.len(),
}
}
fn ffi_test_guard() -> MutexGuard<'static, ()> {
static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
TEST_MUTEX
.get_or_init(|| Mutex::new(()))
.lock()
.expect("lock ffi test mutex")
}
struct TestFfiEngineHandle {
engine_id: u64,
}
impl Drop for TestFfiEngineHandle {
fn drop(&mut self) {
if let Ok(mut registry) = ffi_engine_registry().lock() {
registry.remove(&self.engine_id);
}
}
}
fn register_test_engine() -> TestFfiEngineHandle {
let engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions::default(),
))
.expect("create ffi test engine");
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
ffi_engine_registry()
.lock()
.expect("lock ffi engine registry")
.insert(engine_id, FfiEngineSlot::new(engine));
TestFfiEngineHandle { engine_id }
}
#[test]
fn ffi_engine_new_and_free_roundtrip() {
let _guard = ffi_test_guard();
let temp_root =
std::env::temp_dir().join(format!("luaskills_ffi_engine_test_{}", std::process::id()));
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
temp_dir: Some(temp_root.join("temp")),
resources_dir: Some(temp_root.join("resources")),
lua_packages_dir: Some(temp_root.join("lua_packages")),
host_provided_tool_root: Some(temp_root.join("bin").join("tools")),
host_provided_lua_root: Some(temp_root.join("lua_packages")),
host_provided_ffi_root: Some(temp_root.join("libs")),
download_cache_root: Some(temp_root.join("temp").join("downloads")),
dependency_dir_name: "dependencies".to_string(),
state_dir_name: "state".to_string(),
database_dir_name: "databases".to_string(),
skill_config_file_path: None,
protection: Default::default(),
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
sqlite_library_path: None,
sqlite_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
sqlite_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
lancedb_library_path: None,
lancedb_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
lancedb_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
space_controller: crate::LuaRuntimeSpaceControllerOptions::default(),
cache_config: None,
runlua_pool_config: None,
reserved_entry_names: Vec::new(),
ignored_skill_ids: Vec::new(),
capabilities: Default::default(),
},
),
};
let input = CString::new(serde_json::to_string(&request).expect("request json"))
.expect("request cstring");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(&input)))
};
assert_eq!(response["ok"], true);
let result: EngineHandleJsonResult =
serde_json::from_value(response["result"].clone()).expect("engine result should parse");
let free_request = CString::new(
serde_json::to_string(&EngineIdJsonRequest {
engine_id: result.engine_id,
})
.expect("free request json"),
)
.expect("free request cstring");
let free_response = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(free_response["ok"], true);
}
#[test]
fn ffi_skill_config_json_roundtrip() {
let _guard = ffi_test_guard();
let temp_root = std::env::temp_dir().join(format!(
"luaskills_ffi_skill_config_json_test_{}",
std::process::id()
));
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
temp_dir: Some(temp_root.join("temp")),
resources_dir: Some(temp_root.join("resources")),
lua_packages_dir: Some(temp_root.join("lua_packages")),
host_provided_tool_root: Some(temp_root.join("bin").join("tools")),
host_provided_lua_root: Some(temp_root.join("lua_packages")),
host_provided_ffi_root: Some(temp_root.join("libs")),
download_cache_root: Some(temp_root.join("temp").join("downloads")),
dependency_dir_name: "dependencies".to_string(),
state_dir_name: "state".to_string(),
database_dir_name: "databases".to_string(),
skill_config_file_path: Some(
temp_root.join("config").join("skill_config.json"),
),
protection: Default::default(),
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
sqlite_library_path: None,
sqlite_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
sqlite_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
lancedb_library_path: None,
lancedb_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
lancedb_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
space_controller: crate::LuaRuntimeSpaceControllerOptions::default(),
cache_config: None,
runlua_pool_config: None,
reserved_entry_names: Vec::new(),
ignored_skill_ids: Vec::new(),
capabilities: Default::default(),
},
),
};
let input = CString::new(serde_json::to_string(&request).expect("request json"))
.expect("request cstring");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(&input)))
};
assert_eq!(response["ok"], true);
let result: EngineHandleJsonResult =
serde_json::from_value(response["result"].clone()).expect("engine result should parse");
let set_request = CString::new(
serde_json::to_string(&SkillConfigSetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
value: "sk-json-ffi".to_string(),
})
.expect("set request json"),
)
.expect("set request cstring");
let set_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_set_json(borrowed_json_buffer(
&set_request,
)))
};
assert_eq!(set_response["ok"], true);
assert_eq!(set_response["result"]["action"], "set");
assert_eq!(set_response["result"]["skill_id"], "demo-skill");
assert_eq!(set_response["result"]["key"], "api_token");
assert_eq!(set_response["result"]["value"], "sk-json-ffi");
let get_request = CString::new(
serde_json::to_string(&SkillConfigGetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
})
.expect("get request json"),
)
.expect("get request cstring");
let get_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_get_json(borrowed_json_buffer(
&get_request,
)))
};
assert_eq!(get_response["ok"], true);
assert_eq!(get_response["result"]["found"], true);
assert_eq!(get_response["result"]["value"], "sk-json-ffi");
let list_request = CString::new(
serde_json::to_string(&SkillConfigListJsonRequest {
engine_id: result.engine_id,
skill_id: Some("demo-skill".to_string()),
})
.expect("list request json"),
)
.expect("list request cstring");
let list_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_list_json(borrowed_json_buffer(
&list_request,
)))
};
assert_eq!(list_response["ok"], true);
assert_eq!(list_response["result"].as_array().map(Vec::len), Some(1));
assert_eq!(list_response["result"][0]["skill_id"], "demo-skill");
assert_eq!(list_response["result"][0]["key"], "api_token");
assert_eq!(list_response["result"][0]["value"], "sk-json-ffi");
let delete_request = CString::new(
serde_json::to_string(&SkillConfigGetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
})
.expect("delete request json"),
)
.expect("delete request cstring");
let delete_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_delete_json(
borrowed_json_buffer(&delete_request),
))
};
assert_eq!(delete_response["ok"], true);
assert_eq!(delete_response["result"]["action"], "delete");
assert_eq!(delete_response["result"]["deleted"], true);
let free_request = CString::new(
serde_json::to_string(&EngineIdJsonRequest {
engine_id: result.engine_id,
})
.expect("free request json"),
)
.expect("free request cstring");
let free_response = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(free_response["ok"], true);
}
#[test]
fn with_engine_releases_registry_lock_before_operation() {
let _guard = ffi_test_guard();
let handle = register_test_engine();
let result = with_engine(handle.engine_id, |_engine| {
let registry_lock = ffi_engine_registry().try_lock();
assert!(
registry_lock.is_ok(),
"registry lock should be acquirable while engine operation is running"
);
Ok(())
});
assert!(result.is_ok());
}
#[test]
fn with_engine_rejects_same_thread_reentry() {
let _guard = ffi_test_guard();
let handle = register_test_engine();
let outer_result = with_engine(handle.engine_id, |_engine| {
let nested_result = with_engine(handle.engine_id, |_nested| Ok(()));
let nested_error = nested_result.expect_err("same-thread reentry should fail");
assert!(nested_error.contains("reentrant access"));
Ok(())
});
assert!(outer_result.is_ok());
}
}