use std::ffi::{CStr, CString, c_char, c_void};
use std::path::PathBuf;
use std::ptr;
use std::sync::atomic::Ordering;
use serde_json::Value;
use crate::ffi::{FFI_ENGINE_COUNTER, ffi_engine_registry, with_engine, with_engine_mut};
use crate::host::callbacks::{
RuntimeHostToolCallback, RuntimeHostToolRequest, set_host_tool_callback,
};
use crate::host::database::{
LuaRuntimeDatabaseCallbackMode, LuaRuntimeDatabaseProviderMode, RuntimeDatabaseBindingContext,
RuntimeDatabaseKind, RuntimeLanceDbProviderAction, RuntimeLanceDbProviderCallback,
RuntimeLanceDbProviderRequest, RuntimeLanceDbProviderResult, RuntimeSqliteProviderAction,
RuntimeSqliteProviderCallback, RuntimeSqliteProviderRequest, set_lancedb_provider_callback,
set_lancedb_provider_json_callback, set_sqlite_provider_callback,
set_sqlite_provider_json_callback,
};
use crate::runtime_context::RuntimeRequestContext;
use crate::runtime_help::{
RuntimeHelpDetail, RuntimeHelpNodeDescriptor, RuntimeSkillHelpDescriptor,
};
use crate::runtime_options::{
LuaInvocationContext, LuaRuntimeCapabilityOptions, LuaRuntimeHostOptions,
LuaRuntimeRunLuaPoolConfig, LuaRuntimeSpaceControllerOptions,
LuaRuntimeSpaceControllerProcessMode, RuntimeSkillRoot,
};
use crate::skill::manager::{SkillInstallRequest, SkillManagementAuthority, SkillUninstallOptions};
use crate::skill::source::SkillInstallSourceType;
use crate::tool_cache::ToolCacheConfig;
use crate::{
LuaEngine, LuaEngineOptions, LuaVmPoolConfig, RuntimeEntryDescriptor,
RuntimeEntryParameterDescriptor, RuntimeInvocationResult, SkillApplyResult,
SkillUninstallResult,
};
const FFI_STATUS_OK: i32 = 0;
const FFI_STATUS_ERROR: i32 = 1;
const FFI_SOURCE_TYPE_ABSENT: i32 = -1;
const FFI_SOURCE_TYPE_GITHUB: i32 = 0;
const FFI_SOURCE_TYPE_URL: i32 = 1;
const FFI_PROVIDER_MODE_DYNAMIC_LIBRARY: i32 = 0;
const FFI_PROVIDER_MODE_HOST_CALLBACK: i32 = 1;
const FFI_PROVIDER_MODE_SPACE_CONTROLLER: i32 = 2;
const FFI_CALLBACK_MODE_STANDARD: i32 = 0;
const FFI_CALLBACK_MODE_JSON: i32 = 1;
const FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE: i32 = 0;
const FFI_SPACE_CONTROLLER_PROCESS_MODE_MANAGED: i32 = 1;
const FFI_DATABASE_KIND_SQLITE: i32 = 0;
const FFI_DATABASE_KIND_LANCEDB: i32 = 1;
const FFI_SQLITE_PROVIDER_ACTION_EXECUTE_SCRIPT: i32 = 0;
const FFI_SQLITE_PROVIDER_ACTION_EXECUTE_BATCH: i32 = 1;
const FFI_SQLITE_PROVIDER_ACTION_QUERY_JSON: i32 = 2;
const FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM: i32 = 3;
const FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_WAIT_METRICS: i32 = 4;
const FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_CHUNK: i32 = 5;
const FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_CLOSE: i32 = 6;
const FFI_SQLITE_PROVIDER_ACTION_TOKENIZE_TEXT: i32 = 7;
const FFI_SQLITE_PROVIDER_ACTION_UPSERT_CUSTOM_WORD: i32 = 8;
const FFI_SQLITE_PROVIDER_ACTION_REMOVE_CUSTOM_WORD: i32 = 9;
const FFI_SQLITE_PROVIDER_ACTION_LIST_CUSTOM_WORDS: i32 = 10;
const FFI_SQLITE_PROVIDER_ACTION_ENSURE_FTS_INDEX: i32 = 11;
const FFI_SQLITE_PROVIDER_ACTION_REBUILD_FTS_INDEX: i32 = 12;
const FFI_SQLITE_PROVIDER_ACTION_UPSERT_FTS_DOCUMENT: i32 = 13;
const FFI_SQLITE_PROVIDER_ACTION_DELETE_FTS_DOCUMENT: i32 = 14;
const FFI_SQLITE_PROVIDER_ACTION_SEARCH_FTS: i32 = 15;
const FFI_LANCEDB_PROVIDER_ACTION_CREATE_TABLE: i32 = 0;
const FFI_LANCEDB_PROVIDER_ACTION_VECTOR_UPSERT: i32 = 1;
const FFI_LANCEDB_PROVIDER_ACTION_VECTOR_SEARCH: i32 = 2;
const FFI_LANCEDB_PROVIDER_ACTION_DELETE: i32 = 3;
const FFI_LANCEDB_PROVIDER_ACTION_DROP_TABLE: i32 = 4;
const FFI_SKILL_AUTHORITY_SYSTEM: i32 = 0;
const FFI_SKILL_AUTHORITY_DELEGATED_TOOL: i32 = 1;
#[repr(C)]
pub struct FfiLuaVmPoolConfig {
pub min_size: usize,
pub max_size: usize,
pub idle_ttl_secs: u64,
}
#[repr(C)]
pub struct FfiToolCacheConfig {
pub max_entries: usize,
pub default_ttl_secs: u64,
pub max_ttl_secs: u64,
}
#[repr(C)]
pub struct FfiBorrowedBuffer {
pub ptr: *const u8,
pub len: usize,
}
#[repr(C)]
pub struct FfiOwnedBuffer {
pub ptr: *mut u8,
pub len: usize,
}
#[repr(C)]
pub struct FfiLuaRuntimeHostOptions {
pub temp_dir: *const c_char,
pub resources_dir: *const c_char,
pub lua_packages_dir: *const c_char,
pub host_provided_tool_root: *const c_char,
pub host_provided_lua_root: *const c_char,
pub host_provided_ffi_root: *const c_char,
pub download_cache_root: *const c_char,
pub dependency_dir_name: *const c_char,
pub state_dir_name: *const c_char,
pub database_dir_name: *const c_char,
pub skill_config_file_path: *const c_char,
pub allow_network_download: u8,
pub github_base_url: *const c_char,
pub github_api_base_url: *const c_char,
pub sqlite_library_path: *const c_char,
pub sqlite_provider_mode: i32,
pub sqlite_callback_mode: i32,
pub lancedb_library_path: *const c_char,
pub lancedb_provider_mode: i32,
pub lancedb_callback_mode: i32,
pub space_controller_endpoint: *const c_char,
pub space_controller_auto_spawn: u8,
pub space_controller_executable_path: *const c_char,
pub space_controller_process_mode: i32,
pub cache_config: *const FfiToolCacheConfig,
pub runlua_pool_config: *const FfiLuaVmPoolConfig,
pub reserved_entry_names: *const *const c_char,
pub reserved_entry_names_len: usize,
pub ignored_skill_ids: *const *const c_char,
pub ignored_skill_ids_len: usize,
pub enable_skill_management_bridge: u8,
}
pub type FfiJsonProviderCallback = unsafe extern "C" fn(
request_json: FfiBorrowedBuffer,
user_data: *mut c_void,
response_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32;
pub type FfiSqliteProviderCallback = unsafe extern "C" fn(
request: *const FfiSqliteProviderRequest,
user_data: *mut c_void,
response_json_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32;
pub type FfiLanceDbProviderCallback = unsafe extern "C" fn(
request: *const FfiLanceDbProviderRequest,
user_data: *mut c_void,
meta_json_out: *mut FfiOwnedBuffer,
data_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32;
#[repr(C)]
pub struct FfiLuaEngineOptions {
pub pool: FfiLuaVmPoolConfig,
pub host: FfiLuaRuntimeHostOptions,
}
#[repr(C)]
pub struct FfiRuntimeSkillRoot {
pub name: *const c_char,
pub skills_dir: *const c_char,
}
#[repr(C)]
pub struct FfiLuaInvocationContext {
pub request_context_json: FfiBorrowedBuffer,
pub client_budget_json: FfiBorrowedBuffer,
pub tool_config_json: FfiBorrowedBuffer,
}
#[repr(C)]
pub struct FfiSkillInstallRequest {
pub skill_id: *const c_char,
pub source: *const c_char,
pub source_type: i32,
}
#[repr(C)]
pub struct FfiSkillUninstallOptions {
pub remove_sqlite: u8,
pub remove_lancedb: u8,
}
#[repr(C)]
pub struct FfiRuntimeDatabaseBindingContext {
pub space_label: *const c_char,
pub skill_id: *const c_char,
pub binding_tag: *const c_char,
pub root_name: *const c_char,
pub space_root: *const c_char,
pub skill_dir: *const c_char,
pub skill_dir_name: *const c_char,
pub database_kind: i32,
pub default_database_path: *const c_char,
}
#[repr(C)]
pub struct FfiSqliteProviderRequest {
pub action: i32,
pub binding: FfiRuntimeDatabaseBindingContext,
pub input_json: FfiBorrowedBuffer,
}
#[repr(C)]
pub struct FfiLanceDbProviderRequest {
pub action: i32,
pub binding: FfiRuntimeDatabaseBindingContext,
pub input_json: FfiBorrowedBuffer,
}
#[repr(C)]
pub struct FfiStringArray {
pub items: *mut FfiOwnedBuffer,
pub len: usize,
}
#[repr(C)]
pub struct FfiRuntimeEntryParameterDescriptor {
pub name: FfiOwnedBuffer,
pub param_type: FfiOwnedBuffer,
pub description: FfiOwnedBuffer,
pub required: u8,
}
#[repr(C)]
pub struct FfiRuntimeEntryDescriptor {
pub canonical_name: FfiOwnedBuffer,
pub skill_id: FfiOwnedBuffer,
pub local_name: FfiOwnedBuffer,
pub root_name: FfiOwnedBuffer,
pub skill_dir: FfiOwnedBuffer,
pub description: FfiOwnedBuffer,
pub parameters: *mut FfiRuntimeEntryParameterDescriptor,
pub parameters_len: usize,
}
#[repr(C)]
pub struct FfiRuntimeEntryDescriptorList {
pub items: *mut FfiRuntimeEntryDescriptor,
pub len: usize,
}
#[repr(C)]
pub struct FfiRuntimeHelpNodeDescriptor {
pub flow_name: FfiOwnedBuffer,
pub description: FfiOwnedBuffer,
pub related_entries: *mut FfiOwnedBuffer,
pub related_entries_len: usize,
pub is_main: u8,
}
#[repr(C)]
pub struct FfiRuntimeSkillHelpDescriptor {
pub skill_id: FfiOwnedBuffer,
pub skill_name: FfiOwnedBuffer,
pub skill_version: FfiOwnedBuffer,
pub root_name: FfiOwnedBuffer,
pub skill_dir: FfiOwnedBuffer,
pub main: FfiRuntimeHelpNodeDescriptor,
pub flows: *mut FfiRuntimeHelpNodeDescriptor,
pub flows_len: usize,
}
#[repr(C)]
pub struct FfiRuntimeSkillHelpDescriptorList {
pub items: *mut FfiRuntimeSkillHelpDescriptor,
pub len: usize,
}
#[repr(C)]
pub struct FfiRuntimeHelpDetail {
pub skill_id: FfiOwnedBuffer,
pub skill_name: FfiOwnedBuffer,
pub skill_version: FfiOwnedBuffer,
pub root_name: FfiOwnedBuffer,
pub skill_dir: FfiOwnedBuffer,
pub flow_name: FfiOwnedBuffer,
pub description: FfiOwnedBuffer,
pub related_entries: *mut FfiOwnedBuffer,
pub related_entries_len: usize,
pub is_main: u8,
pub content_type: FfiOwnedBuffer,
pub content: FfiOwnedBuffer,
}
#[repr(C)]
pub struct FfiRuntimeInvocationResult {
pub content: FfiOwnedBuffer,
pub overflow_mode: i32,
pub template_hint: FfiOwnedBuffer,
pub content_bytes: usize,
pub content_lines: usize,
}
#[repr(C)]
pub struct FfiSkillApplyResult {
pub skill_id: FfiOwnedBuffer,
pub status: FfiOwnedBuffer,
pub message: FfiOwnedBuffer,
pub version: FfiOwnedBuffer,
pub source_type: i32,
pub source_locator: FfiOwnedBuffer,
}
#[repr(C)]
pub struct FfiSkillUninstallResult {
pub skill_id: FfiOwnedBuffer,
pub skill_removed: u8,
pub sqlite_removed: u8,
pub lancedb_removed: u8,
pub sqlite_retained: u8,
pub lancedb_retained: u8,
pub message: FfiOwnedBuffer,
}
fn set_error_out(error_out: *mut FfiOwnedBuffer, message: impl Into<String>) {
if error_out.is_null() {
return;
}
let text = message.into();
unsafe {
*error_out = alloc_owned_buffer_from_bytes(text.as_bytes());
}
}
fn clear_error_out(error_out: *mut FfiOwnedBuffer) {
clear_out_buffer(error_out);
}
fn clear_out_ptr<T>(value_out: *mut *mut T) {
if !value_out.is_null() {
unsafe { *value_out = std::ptr::null_mut() };
}
}
fn clear_out_buffer(value_out: *mut FfiOwnedBuffer) {
if !value_out.is_null() {
unsafe {
*value_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
}
};
}
}
fn clear_out_u64(value_out: *mut u64) {
if !value_out.is_null() {
unsafe { *value_out = 0 };
}
}
fn clear_out_u8(value_out: *mut u8) {
if !value_out.is_null() {
unsafe { *value_out = 0 };
}
}
fn alloc_c_string(value: impl AsRef<str>) -> *mut c_char {
CString::new(value.as_ref())
.unwrap_or_else(|_| CString::new("FFI string contains NUL byte").expect("static text"))
.into_raw()
}
fn alloc_owned_buffer_from_bytes(value: &[u8]) -> FfiOwnedBuffer {
if value.is_empty() {
return FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
let mut bytes = value.to_vec();
let pointer = bytes.as_mut_ptr();
let len = bytes.len();
std::mem::forget(bytes);
FfiOwnedBuffer { ptr: pointer, len }
}
fn alloc_owned_buffer_from_string(value: impl AsRef<str>) -> FfiOwnedBuffer {
alloc_owned_buffer_from_bytes(value.as_ref().as_bytes())
}
fn to_cstring(value: impl AsRef<str>, field_name: &str) -> Result<CString, String> {
CString::new(value.as_ref()).map_err(|_| format!("{} contains interior NUL bytes", field_name))
}
fn alloc_optional_owned_buffer_from_string(value: Option<&str>) -> FfiOwnedBuffer {
value.map_or(
FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
},
alloc_owned_buffer_from_string,
)
}
fn parse_required_string(value: *const c_char, field_name: &str) -> Result<String, String> {
if value.is_null() {
return Err(format!("{} must not be null", field_name));
}
let text = unsafe { CStr::from_ptr(value) }
.to_str()
.map_err(|error| format!("{} contains invalid UTF-8: {}", field_name, error))?;
if text.is_empty() {
return Err(format!("{} must not be empty", field_name));
}
Ok(text.to_string())
}
fn parse_required_string_allow_empty(
value: *const c_char,
field_name: &str,
) -> Result<String, String> {
if value.is_null() {
return Err(format!("{} must not be null", field_name));
}
unsafe { CStr::from_ptr(value) }
.to_str()
.map(|text| text.to_string())
.map_err(|error| format!("{} contains invalid UTF-8: {}", field_name, error))
}
fn parse_optional_string(value: *const c_char, field_name: &str) -> Result<Option<String>, String> {
if value.is_null() {
return Ok(None);
}
let text = unsafe { CStr::from_ptr(value) }
.to_str()
.map_err(|error| format!("{} contains invalid UTF-8: {}", field_name, error))?;
if text.is_empty() {
return Ok(None);
}
Ok(Some(text.to_string()))
}
fn parse_string_array(
items: *const *const c_char,
len: usize,
field_name: &str,
) -> Result<Vec<String>, String> {
if len == 0 {
return Ok(Vec::new());
}
if items.is_null() {
return Err(format!(
"{} items pointer must not be null when len > 0",
field_name
));
}
let slice = unsafe { std::slice::from_raw_parts(items, len) };
slice
.iter()
.enumerate()
.map(|(index, item)| parse_required_string(*item, &format!("{}[{}]", field_name, index)))
.collect()
}
fn parse_optional_borrowed_text(
value: &FfiBorrowedBuffer,
field_name: &str,
) -> Result<Option<String>, String> {
if value.len == 0 {
return Ok(None);
}
if value.ptr.is_null() {
return Err(format!(
"{} pointer must not be null when len > 0",
field_name
));
}
let bytes = unsafe { std::slice::from_raw_parts(value.ptr, value.len) };
let text = std::str::from_utf8(bytes)
.map_err(|error| format!("{} contains invalid UTF-8: {}", field_name, error))?;
if text.is_empty() {
return Ok(None);
}
Ok(Some(text.to_string()))
}
fn parse_json_value_or_empty_object_buffer(
value: &FfiBorrowedBuffer,
field_name: &str,
) -> Result<Value, String> {
match parse_optional_borrowed_text(value, field_name)? {
Some(text) => serde_json::from_str(&text)
.map_err(|error| format!("{} contains invalid JSON: {}", field_name, error)),
None => Ok(Value::Object(serde_json::Map::new())),
}
}
fn parse_request_context_buffer(
value: &FfiBorrowedBuffer,
field_name: &str,
) -> Result<Option<RuntimeRequestContext>, String> {
match parse_optional_borrowed_text(value, field_name)? {
Some(text) => serde_json::from_str(&text)
.map(Some)
.map_err(|error| format!("{} contains invalid JSON: {}", field_name, error)),
None => Ok(None),
}
}
fn parse_cache_config(value: *const FfiToolCacheConfig) -> Option<ToolCacheConfig> {
if value.is_null() {
None
} else {
let config = unsafe { &*value };
Some(ToolCacheConfig {
max_entries: config.max_entries,
default_ttl_secs: config.default_ttl_secs,
max_ttl_secs: config.max_ttl_secs,
})
}
}
fn parse_runlua_pool_config(
value: *const FfiLuaVmPoolConfig,
) -> Option<LuaRuntimeRunLuaPoolConfig> {
if value.is_null() {
None
} else {
let config = unsafe { &*value };
Some(LuaRuntimeRunLuaPoolConfig {
min_size: config.min_size,
max_size: config.max_size,
idle_ttl_secs: config.idle_ttl_secs,
})
}
}
fn parse_host_options(value: &FfiLuaRuntimeHostOptions) -> Result<LuaRuntimeHostOptions, String> {
Ok(LuaRuntimeHostOptions {
temp_dir: parse_optional_string(value.temp_dir, "temp_dir")?.map(PathBuf::from),
resources_dir: parse_optional_string(value.resources_dir, "resources_dir")?
.map(PathBuf::from),
lua_packages_dir: parse_optional_string(value.lua_packages_dir, "lua_packages_dir")?
.map(PathBuf::from),
host_provided_tool_root: parse_optional_string(
value.host_provided_tool_root,
"host_provided_tool_root",
)?
.map(PathBuf::from),
host_provided_lua_root: parse_optional_string(
value.host_provided_lua_root,
"host_provided_lua_root",
)?
.map(PathBuf::from),
host_provided_ffi_root: parse_optional_string(
value.host_provided_ffi_root,
"host_provided_ffi_root",
)?
.map(PathBuf::from),
download_cache_root: parse_optional_string(
value.download_cache_root,
"download_cache_root",
)?
.map(PathBuf::from),
dependency_dir_name: parse_required_string(
value.dependency_dir_name,
"dependency_dir_name",
)?,
state_dir_name: parse_required_string(value.state_dir_name, "state_dir_name")?,
database_dir_name: parse_required_string(value.database_dir_name, "database_dir_name")?,
skill_config_file_path: parse_optional_string(
value.skill_config_file_path,
"skill_config_file_path",
)?
.map(PathBuf::from),
allow_network_download: value.allow_network_download != 0,
github_base_url: parse_optional_string(value.github_base_url, "github_base_url")?,
github_api_base_url: parse_optional_string(
value.github_api_base_url,
"github_api_base_url",
)?,
sqlite_library_path: parse_optional_string(
value.sqlite_library_path,
"sqlite_library_path",
)?
.map(PathBuf::from),
sqlite_provider_mode: parse_provider_mode(
value.sqlite_provider_mode,
"sqlite_provider_mode",
)?,
sqlite_callback_mode: parse_callback_mode(
value.sqlite_callback_mode,
"sqlite_callback_mode",
)?,
lancedb_library_path: parse_optional_string(
value.lancedb_library_path,
"lancedb_library_path",
)?
.map(PathBuf::from),
lancedb_provider_mode: parse_provider_mode(
value.lancedb_provider_mode,
"lancedb_provider_mode",
)?,
lancedb_callback_mode: parse_callback_mode(
value.lancedb_callback_mode,
"lancedb_callback_mode",
)?,
space_controller: LuaRuntimeSpaceControllerOptions {
endpoint: parse_optional_string(
value.space_controller_endpoint,
"space_controller_endpoint",
)?,
auto_spawn: value.space_controller_auto_spawn != 0,
executable_path: parse_optional_string(
value.space_controller_executable_path,
"space_controller_executable_path",
)?
.map(PathBuf::from),
process_mode: parse_space_controller_process_mode(
value.space_controller_process_mode,
"space_controller_process_mode",
)?,
..LuaRuntimeSpaceControllerOptions::default()
},
cache_config: parse_cache_config(value.cache_config),
runlua_pool_config: parse_runlua_pool_config(value.runlua_pool_config),
reserved_entry_names: parse_string_array(
value.reserved_entry_names,
value.reserved_entry_names_len,
"reserved_entry_names",
)?,
ignored_skill_ids: parse_string_array(
value.ignored_skill_ids,
value.ignored_skill_ids_len,
"ignored_skill_ids",
)?,
capabilities: LuaRuntimeCapabilityOptions {
enable_skill_management_bridge: value.enable_skill_management_bridge != 0,
},
})
}
fn parse_provider_mode(
value: i32,
field_name: &str,
) -> Result<LuaRuntimeDatabaseProviderMode, String> {
match value {
FFI_PROVIDER_MODE_DYNAMIC_LIBRARY => Ok(LuaRuntimeDatabaseProviderMode::DynamicLibrary),
FFI_PROVIDER_MODE_HOST_CALLBACK => Ok(LuaRuntimeDatabaseProviderMode::HostCallback),
FFI_PROVIDER_MODE_SPACE_CONTROLLER => Ok(LuaRuntimeDatabaseProviderMode::SpaceController),
_ => Err(format!("Unsupported {} value '{}'", field_name, value)),
}
}
fn parse_skill_management_authority(
value: i32,
field_name: &str,
) -> Result<SkillManagementAuthority, String> {
match value {
FFI_SKILL_AUTHORITY_SYSTEM => Ok(SkillManagementAuthority::System),
FFI_SKILL_AUTHORITY_DELEGATED_TOOL => Ok(SkillManagementAuthority::DelegatedTool),
other => Err(format!(
"{} must be 0 (system) or 1 (delegated_tool); got {}",
field_name, other
)),
}
}
fn parse_callback_mode(
value: i32,
field_name: &str,
) -> Result<LuaRuntimeDatabaseCallbackMode, String> {
match value {
FFI_CALLBACK_MODE_STANDARD => Ok(LuaRuntimeDatabaseCallbackMode::Standard),
FFI_CALLBACK_MODE_JSON => Ok(LuaRuntimeDatabaseCallbackMode::Json),
_ => Err(format!("Unsupported {} value '{}'", field_name, value)),
}
}
fn parse_space_controller_process_mode(
value: i32,
field_name: &str,
) -> Result<LuaRuntimeSpaceControllerProcessMode, String> {
match value {
FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE => {
Ok(LuaRuntimeSpaceControllerProcessMode::Service)
}
FFI_SPACE_CONTROLLER_PROCESS_MODE_MANAGED => {
Ok(LuaRuntimeSpaceControllerProcessMode::Managed)
}
_ => Err(format!("Unsupported {} value '{}'", field_name, value)),
}
}
fn parse_engine_options(value: &FfiLuaEngineOptions) -> Result<LuaEngineOptions, String> {
Ok(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: value.pool.min_size,
max_size: value.pool.max_size,
idle_ttl_secs: value.pool.idle_ttl_secs,
},
parse_host_options(&value.host)?,
))
}
fn parse_skill_roots(
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
) -> Result<Vec<RuntimeSkillRoot>, String> {
if skill_roots_len == 0 {
return Ok(Vec::new());
}
if skill_roots.is_null() {
return Err("skill_roots pointer must not be null when len > 0".to_string());
}
let roots = unsafe { std::slice::from_raw_parts(skill_roots, skill_roots_len) };
roots
.iter()
.enumerate()
.map(|(index, root)| {
Ok(RuntimeSkillRoot {
name: parse_required_string(root.name, &format!("skill_roots[{}].name", index))?,
skills_dir: PathBuf::from(parse_required_string(
root.skills_dir,
&format!("skill_roots[{}].skills_dir", index),
)?),
})
})
.collect()
}
fn parse_invocation_context(
value: *const FfiLuaInvocationContext,
) -> Result<Option<LuaInvocationContext>, String> {
if value.is_null() {
return Ok(None);
}
let context = unsafe { &*value };
Ok(Some(LuaInvocationContext::new(
parse_request_context_buffer(&context.request_context_json, "request_context_json")?,
parse_json_value_or_empty_object_buffer(&context.client_budget_json, "client_budget_json")?,
parse_json_value_or_empty_object_buffer(&context.tool_config_json, "tool_config_json")?,
)))
}
fn parse_source_type(value: i32) -> Result<SkillInstallSourceType, String> {
match value {
FFI_SOURCE_TYPE_GITHUB => Ok(SkillInstallSourceType::Github),
FFI_SOURCE_TYPE_URL => Ok(SkillInstallSourceType::Url),
_ => Err(format!("Unsupported source_type '{}'", value)),
}
}
fn parse_install_request(value: &FfiSkillInstallRequest) -> Result<SkillInstallRequest, String> {
Ok(SkillInstallRequest {
skill_id: parse_optional_string(value.skill_id, "skill_id")?,
source: parse_optional_string(value.source, "source")?,
source_type: parse_source_type(value.source_type)?,
})
}
fn parse_uninstall_options(value: Option<&FfiSkillUninstallOptions>) -> SkillUninstallOptions {
match value {
Some(value) => SkillUninstallOptions {
remove_sqlite: value.remove_sqlite != 0,
remove_lancedb: value.remove_lancedb != 0,
},
None => SkillUninstallOptions::default(),
}
}
fn alloc_string_array(values: &[String]) -> FfiStringArray {
let mut items: Vec<FfiOwnedBuffer> =
values.iter().map(alloc_owned_buffer_from_string).collect();
let result = FfiStringArray {
items: items.as_mut_ptr(),
len: items.len(),
};
std::mem::forget(items);
result
}
fn alloc_entry_parameter_descriptor(
value: &RuntimeEntryParameterDescriptor,
) -> FfiRuntimeEntryParameterDescriptor {
FfiRuntimeEntryParameterDescriptor {
name: alloc_owned_buffer_from_string(&value.name),
param_type: alloc_owned_buffer_from_string(&value.param_type),
description: alloc_owned_buffer_from_string(&value.description),
required: u8::from(value.required),
}
}
fn alloc_entry_descriptor(value: &RuntimeEntryDescriptor) -> FfiRuntimeEntryDescriptor {
let mut parameters: Vec<FfiRuntimeEntryParameterDescriptor> = value
.parameters
.iter()
.map(alloc_entry_parameter_descriptor)
.collect();
let parameters_ptr = parameters.as_mut_ptr();
let parameters_len = parameters.len();
std::mem::forget(parameters);
FfiRuntimeEntryDescriptor {
canonical_name: alloc_owned_buffer_from_string(&value.canonical_name),
skill_id: alloc_owned_buffer_from_string(&value.skill_id),
local_name: alloc_owned_buffer_from_string(&value.local_name),
root_name: alloc_owned_buffer_from_string(&value.root_name),
skill_dir: alloc_owned_buffer_from_string(&value.skill_dir),
description: alloc_owned_buffer_from_string(&value.description),
parameters: parameters_ptr,
parameters_len,
}
}
fn alloc_help_node_descriptor(value: &RuntimeHelpNodeDescriptor) -> FfiRuntimeHelpNodeDescriptor {
let related_entries = alloc_string_array(&value.related_entries);
FfiRuntimeHelpNodeDescriptor {
flow_name: alloc_owned_buffer_from_string(&value.flow_name),
description: alloc_owned_buffer_from_string(&value.description),
related_entries: related_entries.items,
related_entries_len: related_entries.len,
is_main: u8::from(value.is_main),
}
}
fn alloc_help_descriptor(value: &RuntimeSkillHelpDescriptor) -> FfiRuntimeSkillHelpDescriptor {
let mut flows: Vec<FfiRuntimeHelpNodeDescriptor> =
value.flows.iter().map(alloc_help_node_descriptor).collect();
let flows_ptr = flows.as_mut_ptr();
let flows_len = flows.len();
std::mem::forget(flows);
FfiRuntimeSkillHelpDescriptor {
skill_id: alloc_owned_buffer_from_string(&value.skill_id),
skill_name: alloc_owned_buffer_from_string(&value.skill_name),
skill_version: alloc_owned_buffer_from_string(&value.skill_version),
root_name: alloc_owned_buffer_from_string(&value.root_name),
skill_dir: alloc_owned_buffer_from_string(&value.skill_dir),
main: alloc_help_node_descriptor(&value.main),
flows: flows_ptr,
flows_len,
}
}
fn alloc_help_detail(value: &RuntimeHelpDetail) -> FfiRuntimeHelpDetail {
let related_entries = alloc_string_array(&value.related_entries);
FfiRuntimeHelpDetail {
skill_id: alloc_owned_buffer_from_string(&value.skill_id),
skill_name: alloc_owned_buffer_from_string(&value.skill_name),
skill_version: alloc_owned_buffer_from_string(&value.skill_version),
root_name: alloc_owned_buffer_from_string(&value.root_name),
skill_dir: alloc_owned_buffer_from_string(&value.skill_dir),
flow_name: alloc_owned_buffer_from_string(&value.flow_name),
description: alloc_owned_buffer_from_string(&value.description),
related_entries: related_entries.items,
related_entries_len: related_entries.len,
is_main: u8::from(value.is_main),
content_type: alloc_owned_buffer_from_string(&value.content_type),
content: alloc_owned_buffer_from_string(&value.content),
}
}
fn alloc_invocation_result(value: &RuntimeInvocationResult) -> FfiRuntimeInvocationResult {
let overflow_mode = match value.overflow_mode {
None => 0,
Some(crate::ToolOverflowMode::Truncate) => 1,
Some(crate::ToolOverflowMode::Page) => 2,
};
FfiRuntimeInvocationResult {
content: alloc_owned_buffer_from_string(&value.content),
overflow_mode,
template_hint: alloc_optional_owned_buffer_from_string(value.template_hint.as_deref()),
content_bytes: value.content_bytes,
content_lines: value.content_lines,
}
}
fn alloc_skill_apply_result(value: &SkillApplyResult) -> FfiSkillApplyResult {
let source_type = match value.source_type {
None => FFI_SOURCE_TYPE_ABSENT,
Some(SkillInstallSourceType::Github) => FFI_SOURCE_TYPE_GITHUB,
Some(SkillInstallSourceType::Url) => FFI_SOURCE_TYPE_URL,
};
FfiSkillApplyResult {
skill_id: alloc_owned_buffer_from_string(&value.skill_id),
status: alloc_owned_buffer_from_string(&value.status),
message: alloc_owned_buffer_from_string(&value.message),
version: alloc_optional_owned_buffer_from_string(value.version.as_deref()),
source_type,
source_locator: alloc_optional_owned_buffer_from_string(value.source_locator.as_deref()),
}
}
fn alloc_skill_uninstall_result(value: &SkillUninstallResult) -> FfiSkillUninstallResult {
FfiSkillUninstallResult {
skill_id: alloc_owned_buffer_from_string(&value.skill_id),
skill_removed: u8::from(value.skill_removed),
sqlite_removed: u8::from(value.sqlite_removed),
lancedb_removed: u8::from(value.lancedb_removed),
sqlite_retained: u8::from(value.sqlite_retained),
lancedb_retained: u8::from(value.lancedb_retained),
message: alloc_owned_buffer_from_string(&value.message),
}
}
struct OwnedFfiRuntimeDatabaseBindingContext {
space_label: CString,
skill_id: CString,
binding_tag: CString,
root_name: CString,
space_root: CString,
skill_dir: CString,
skill_dir_name: CString,
default_database_path: CString,
ffi: FfiRuntimeDatabaseBindingContext,
}
impl OwnedFfiRuntimeDatabaseBindingContext {
fn from_runtime(value: &RuntimeDatabaseBindingContext) -> Result<Self, String> {
let space_label = to_cstring(&value.space_label, "space_label")?;
let skill_id = to_cstring(&value.skill_id, "skill_id")?;
let binding_tag = to_cstring(&value.binding_tag, "binding_tag")?;
let root_name = to_cstring(&value.root_name, "root_name")?;
let space_root = to_cstring(&value.space_root, "space_root")?;
let skill_dir = to_cstring(&value.skill_dir, "skill_dir")?;
let skill_dir_name = to_cstring(&value.skill_dir_name, "skill_dir_name")?;
let default_database_path =
to_cstring(&value.default_database_path, "default_database_path")?;
let database_kind = ffi_database_kind_code(value.database_kind);
let ffi = FfiRuntimeDatabaseBindingContext {
space_label: space_label.as_ptr(),
skill_id: skill_id.as_ptr(),
binding_tag: binding_tag.as_ptr(),
root_name: root_name.as_ptr(),
space_root: space_root.as_ptr(),
skill_dir: skill_dir.as_ptr(),
skill_dir_name: skill_dir_name.as_ptr(),
database_kind,
default_database_path: default_database_path.as_ptr(),
};
Ok(Self {
space_label,
skill_id,
binding_tag,
root_name,
space_root,
skill_dir,
skill_dir_name,
default_database_path,
ffi,
})
}
fn as_ffi(&self) -> FfiRuntimeDatabaseBindingContext {
FfiRuntimeDatabaseBindingContext {
space_label: self.space_label.as_ptr(),
skill_id: self.skill_id.as_ptr(),
binding_tag: self.binding_tag.as_ptr(),
root_name: self.root_name.as_ptr(),
space_root: self.space_root.as_ptr(),
skill_dir: self.skill_dir.as_ptr(),
skill_dir_name: self.skill_dir_name.as_ptr(),
database_kind: self.ffi.database_kind,
default_database_path: self.default_database_path.as_ptr(),
}
}
}
fn borrowed_buffer_from_bytes(bytes: &[u8]) -> FfiBorrowedBuffer {
if bytes.is_empty() {
return FfiBorrowedBuffer {
ptr: ptr::null(),
len: 0,
};
}
FfiBorrowedBuffer {
ptr: bytes.as_ptr(),
len: bytes.len(),
}
}
struct OwnedFfiSqliteProviderRequest {
_binding: OwnedFfiRuntimeDatabaseBindingContext,
_input_json: Vec<u8>,
ffi: FfiSqliteProviderRequest,
}
impl OwnedFfiSqliteProviderRequest {
fn from_runtime(value: &RuntimeSqliteProviderRequest) -> Result<Self, String> {
let binding = OwnedFfiRuntimeDatabaseBindingContext::from_runtime(&value.binding)?;
let input_json = serde_json::to_vec(&value.input)
.map_err(|error| format!("failed to encode sqlite input json: {}", error))?;
let ffi = FfiSqliteProviderRequest {
action: ffi_sqlite_provider_action_code(&value.action),
binding: binding.as_ffi(),
input_json: borrowed_buffer_from_bytes(&input_json),
};
Ok(Self {
_binding: binding,
_input_json: input_json,
ffi,
})
}
fn as_ptr(&self) -> *const FfiSqliteProviderRequest {
&self.ffi
}
}
struct OwnedFfiLanceDbProviderRequest {
_binding: OwnedFfiRuntimeDatabaseBindingContext,
_input_json: Vec<u8>,
ffi: FfiLanceDbProviderRequest,
}
impl OwnedFfiLanceDbProviderRequest {
fn from_runtime(value: &RuntimeLanceDbProviderRequest) -> Result<Self, String> {
let binding = OwnedFfiRuntimeDatabaseBindingContext::from_runtime(&value.binding)?;
let input_json = serde_json::to_vec(&value.input)
.map_err(|error| format!("failed to encode lancedb input json: {}", error))?;
let ffi = FfiLanceDbProviderRequest {
action: ffi_lancedb_provider_action_code(&value.action),
binding: binding.as_ffi(),
input_json: borrowed_buffer_from_bytes(&input_json),
};
Ok(Self {
_binding: binding,
_input_json: input_json,
ffi,
})
}
fn as_ptr(&self) -> *const FfiLanceDbProviderRequest {
&self.ffi
}
}
fn ffi_database_kind_code(value: RuntimeDatabaseKind) -> i32 {
match value {
RuntimeDatabaseKind::Sqlite => FFI_DATABASE_KIND_SQLITE,
RuntimeDatabaseKind::LanceDb => FFI_DATABASE_KIND_LANCEDB,
}
}
fn ffi_sqlite_provider_action_code(value: &RuntimeSqliteProviderAction) -> i32 {
match value {
RuntimeSqliteProviderAction::ExecuteScript => FFI_SQLITE_PROVIDER_ACTION_EXECUTE_SCRIPT,
RuntimeSqliteProviderAction::ExecuteBatch => FFI_SQLITE_PROVIDER_ACTION_EXECUTE_BATCH,
RuntimeSqliteProviderAction::QueryJson => FFI_SQLITE_PROVIDER_ACTION_QUERY_JSON,
RuntimeSqliteProviderAction::QueryStream => FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM,
RuntimeSqliteProviderAction::QueryStreamWaitMetrics => {
FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_WAIT_METRICS
}
RuntimeSqliteProviderAction::QueryStreamChunk => {
FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_CHUNK
}
RuntimeSqliteProviderAction::QueryStreamClose => {
FFI_SQLITE_PROVIDER_ACTION_QUERY_STREAM_CLOSE
}
RuntimeSqliteProviderAction::TokenizeText => FFI_SQLITE_PROVIDER_ACTION_TOKENIZE_TEXT,
RuntimeSqliteProviderAction::UpsertCustomWord => {
FFI_SQLITE_PROVIDER_ACTION_UPSERT_CUSTOM_WORD
}
RuntimeSqliteProviderAction::RemoveCustomWord => {
FFI_SQLITE_PROVIDER_ACTION_REMOVE_CUSTOM_WORD
}
RuntimeSqliteProviderAction::ListCustomWords => {
FFI_SQLITE_PROVIDER_ACTION_LIST_CUSTOM_WORDS
}
RuntimeSqliteProviderAction::EnsureFtsIndex => FFI_SQLITE_PROVIDER_ACTION_ENSURE_FTS_INDEX,
RuntimeSqliteProviderAction::RebuildFtsIndex => {
FFI_SQLITE_PROVIDER_ACTION_REBUILD_FTS_INDEX
}
RuntimeSqliteProviderAction::UpsertFtsDocument => {
FFI_SQLITE_PROVIDER_ACTION_UPSERT_FTS_DOCUMENT
}
RuntimeSqliteProviderAction::DeleteFtsDocument => {
FFI_SQLITE_PROVIDER_ACTION_DELETE_FTS_DOCUMENT
}
RuntimeSqliteProviderAction::SearchFts => FFI_SQLITE_PROVIDER_ACTION_SEARCH_FTS,
}
}
fn ffi_lancedb_provider_action_code(value: &RuntimeLanceDbProviderAction) -> i32 {
match value {
RuntimeLanceDbProviderAction::CreateTable => FFI_LANCEDB_PROVIDER_ACTION_CREATE_TABLE,
RuntimeLanceDbProviderAction::VectorUpsert => FFI_LANCEDB_PROVIDER_ACTION_VECTOR_UPSERT,
RuntimeLanceDbProviderAction::VectorSearch => FFI_LANCEDB_PROVIDER_ACTION_VECTOR_SEARCH,
RuntimeLanceDbProviderAction::Delete => FFI_LANCEDB_PROVIDER_ACTION_DELETE,
RuntimeLanceDbProviderAction::DropTable => FFI_LANCEDB_PROVIDER_ACTION_DROP_TABLE,
}
}
fn invoke_json_provider_callback(
callback: FfiJsonProviderCallback,
user_data: usize,
request_json: &str,
) -> Result<String, String> {
let request_bytes = request_json.as_bytes();
let mut response_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
callback(
FfiBorrowedBuffer {
ptr: request_bytes.as_ptr(),
len: request_bytes.len(),
},
user_data as *mut c_void,
&mut response_out,
&mut error_out,
)
};
let callback_error =
take_optional_owned_ffi_string_buffer(error_out, "json host provider callback error_out")?;
if status != FFI_STATUS_OK {
unsafe { free_ffi_bytes(response_out.ptr, response_out.len) };
return Err(callback_error.unwrap_or_else(|| {
"json host provider callback returned failure without error message".to_string()
}));
}
let response = take_optional_owned_ffi_string_buffer(
response_out,
"json host provider callback response_out",
)?
.ok_or_else(|| "json host provider callback returned empty response_out".to_string())?;
if let Some(message) = callback_error {
if !message.is_empty() {
return Err(format!(
"json host provider callback returned unexpected error text on success: {}",
message
));
}
}
Ok(response)
}
fn invoke_standard_sqlite_provider_callback(
callback: FfiSqliteProviderCallback,
user_data: usize,
request: &RuntimeSqliteProviderRequest,
) -> Result<Value, String> {
let request = OwnedFfiSqliteProviderRequest::from_runtime(request)?;
let mut response_json_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
callback(
request.as_ptr(),
user_data as *mut c_void,
&mut response_json_out,
&mut error_out,
)
};
let callback_error = take_optional_owned_ffi_string_buffer(
error_out,
"sqlite host provider callback error_out",
)?;
if status != FFI_STATUS_OK {
unsafe { free_ffi_bytes(response_json_out.ptr, response_json_out.len) };
return Err(callback_error.unwrap_or_else(|| {
"sqlite host provider callback returned failure without error message".to_string()
}));
}
let response_json = take_optional_owned_ffi_string_buffer(
response_json_out,
"sqlite host provider callback response_json_out",
)?
.ok_or_else(|| "sqlite host provider callback returned empty response_json_out".to_string())?;
if let Some(message) = callback_error {
if !message.is_empty() {
return Err(format!(
"sqlite host provider callback returned unexpected error text on success: {}",
message
));
}
}
serde_json::from_str(&response_json).map_err(|error| {
format!(
"failed to parse sqlite provider callback response json: {}",
error
)
})
}
fn invoke_standard_lancedb_provider_callback(
callback: FfiLanceDbProviderCallback,
user_data: usize,
request: &RuntimeLanceDbProviderRequest,
) -> Result<RuntimeLanceDbProviderResult, String> {
let request = OwnedFfiLanceDbProviderRequest::from_runtime(request)?;
let mut meta_json_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut data_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
callback(
request.as_ptr(),
user_data as *mut c_void,
&mut meta_json_out,
&mut data_out,
&mut error_out,
)
};
let callback_error = take_optional_owned_ffi_string_buffer(
error_out,
"lancedb host provider callback error_out",
)?;
if status != FFI_STATUS_OK {
unsafe {
free_ffi_bytes(meta_json_out.ptr, meta_json_out.len);
free_ffi_bytes(data_out.ptr, data_out.len);
}
return Err(callback_error.unwrap_or_else(|| {
"lancedb host provider callback returned failure without error message".to_string()
}));
}
let meta_json = take_optional_owned_ffi_string_buffer(
meta_json_out,
"lancedb host provider callback meta_json_out",
)?
.unwrap_or_else(|| "{}".to_string());
let meta = serde_json::from_str::<Value>(&meta_json).map_err(|error| {
format!(
"failed to parse lancedb provider callback meta json: {}",
error
)
})?;
let bytes =
take_optional_owned_ffi_buffer(data_out, "lancedb host provider callback data_out")?
.unwrap_or_default();
if let Some(message) = callback_error {
if !message.is_empty() {
return Err(format!(
"lancedb host provider callback returned unexpected error text on success: {}",
message
));
}
}
Ok(RuntimeLanceDbProviderResult::binary(meta, bytes))
}
fn take_optional_owned_ffi_buffer(
value: FfiOwnedBuffer,
field_name: &str,
) -> Result<Option<Vec<u8>>, String> {
if value.ptr.is_null() {
if value.len == 0 {
return Ok(None);
}
return Err(format!(
"{} returned null ptr with non-zero len",
field_name
));
}
let bytes = unsafe { std::slice::from_raw_parts(value.ptr, value.len) }.to_vec();
unsafe { free_ffi_bytes(value.ptr, value.len) };
Ok(Some(bytes))
}
fn take_optional_owned_ffi_string_buffer(
value: FfiOwnedBuffer,
field_name: &str,
) -> Result<Option<String>, String> {
let bytes = match take_optional_owned_ffi_buffer(value, field_name)? {
Some(bytes) => bytes,
None => return Ok(None),
};
String::from_utf8(bytes)
.map(Some)
.map_err(|error| format!("{} returned non-utf8 bytes: {}", field_name, error))
}
unsafe fn free_ffi_bytes(value: *mut u8, len: usize) {
if value.is_null() || len == 0 {
return;
}
let _ = unsafe { Vec::from_raw_parts(value, len, len) };
}
unsafe fn free_string_array_parts(items: *mut FfiOwnedBuffer, len: usize) {
if items.is_null() || len == 0 {
return;
}
let values = unsafe { Vec::from_raw_parts(items, len, len) };
for value in values {
unsafe { luaskills_ffi_buffer_free(value) };
}
}
unsafe fn free_entry_parameter_descriptor(value: FfiRuntimeEntryParameterDescriptor) {
unsafe { luaskills_ffi_buffer_free(value.name) };
unsafe { luaskills_ffi_buffer_free(value.param_type) };
unsafe { luaskills_ffi_buffer_free(value.description) };
}
unsafe fn free_entry_descriptor(value: FfiRuntimeEntryDescriptor) {
unsafe { luaskills_ffi_buffer_free(value.canonical_name) };
unsafe { luaskills_ffi_buffer_free(value.skill_id) };
unsafe { luaskills_ffi_buffer_free(value.local_name) };
unsafe { luaskills_ffi_buffer_free(value.root_name) };
unsafe { luaskills_ffi_buffer_free(value.skill_dir) };
unsafe { luaskills_ffi_buffer_free(value.description) };
if !value.parameters.is_null() && value.parameters_len > 0 {
let parameters = unsafe {
Vec::from_raw_parts(value.parameters, value.parameters_len, value.parameters_len)
};
for parameter in parameters {
unsafe { free_entry_parameter_descriptor(parameter) };
}
}
}
unsafe fn free_help_node_descriptor(value: FfiRuntimeHelpNodeDescriptor) {
unsafe { luaskills_ffi_buffer_free(value.flow_name) };
unsafe { luaskills_ffi_buffer_free(value.description) };
unsafe { free_string_array_parts(value.related_entries, value.related_entries_len) };
}
fn ffi_ok_status(error_out: *mut FfiOwnedBuffer) -> i32 {
clear_error_out(error_out);
FFI_STATUS_OK
}
fn ffi_error_status(error_out: *mut FfiOwnedBuffer, message: impl Into<String>) -> i32 {
set_error_out(error_out, message);
FFI_STATUS_ERROR
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_string_clone(value: *const c_char) -> *mut c_char {
if value.is_null() {
return alloc_c_string("");
}
let text = unsafe { CStr::from_ptr(value) }
.to_string_lossy()
.to_string();
alloc_c_string(&text)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_buffer_clone(
value: *const u8,
len: usize,
buffer_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(buffer_out);
if buffer_out.is_null() {
return ffi_error_status(error_out, "buffer_out must not be null");
}
if value.is_null() && len != 0 {
return ffi_error_status(error_out, "value must not be null when len > 0");
}
let slice = if len == 0 {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(value, len) }
};
unsafe {
*buffer_out = alloc_owned_buffer_from_bytes(slice);
}
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_bytes_clone(value: *const u8, len: usize) -> *mut u8 {
if value.is_null() || len == 0 {
return ptr::null_mut();
}
let slice = unsafe { std::slice::from_raw_parts(value, len) };
let mut bytes = slice.to_vec();
let pointer = bytes.as_mut_ptr();
std::mem::forget(bytes);
pointer
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_bytes_free(value: *mut u8, len: usize) {
unsafe { free_ffi_bytes(value, len) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_buffer_free(value: FfiOwnedBuffer) {
unsafe { free_ffi_bytes(value.ptr, value.len) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_set_sqlite_provider_callback(
callback: Option<FfiSqliteProviderCallback>,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let wrapped = callback.map(|callback_fn| {
let user_data = user_data as usize;
std::sync::Arc::new(move |request: &RuntimeSqliteProviderRequest| {
invoke_standard_sqlite_provider_callback(callback_fn, user_data, request)
}) as RuntimeSqliteProviderCallback
});
set_sqlite_provider_callback(wrapped);
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_set_lancedb_provider_callback(
callback: Option<FfiLanceDbProviderCallback>,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let wrapped = callback.map(|callback_fn| {
let user_data = user_data as usize;
std::sync::Arc::new(move |request: &RuntimeLanceDbProviderRequest| {
invoke_standard_lancedb_provider_callback(callback_fn, user_data, request)
}) as RuntimeLanceDbProviderCallback
});
set_lancedb_provider_callback(wrapped);
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_set_sqlite_provider_json_callback(
callback: Option<FfiJsonProviderCallback>,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let wrapped = callback.map(|callback_fn| {
let user_data = user_data as usize;
std::sync::Arc::new(move |request_json: &str| {
invoke_json_provider_callback(callback_fn, user_data, request_json)
}) as crate::host::database::RuntimeSqliteProviderJsonCallback
});
set_sqlite_provider_json_callback(wrapped);
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_set_lancedb_provider_json_callback(
callback: Option<FfiJsonProviderCallback>,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let wrapped = callback.map(|callback_fn| {
let user_data = user_data as usize;
std::sync::Arc::new(move |request_json: &str| {
invoke_json_provider_callback(callback_fn, user_data, request_json)
}) as crate::host::database::RuntimeLanceDbProviderJsonCallback
});
set_lancedb_provider_json_callback(wrapped);
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_set_host_tool_json_callback(
callback: Option<FfiJsonProviderCallback>,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let wrapped = callback.map(|callback_fn| {
let user_data = user_data as usize;
std::sync::Arc::new(move |request: &RuntimeHostToolRequest| {
let request_json = serde_json::to_string(request)
.map_err(|error| format!("host tool request JSON encode failed: {}", error))?;
let response_json =
invoke_json_provider_callback(callback_fn, user_data, &request_json)?;
serde_json::from_str::<Value>(&response_json)
.map_err(|error| format!("host tool response JSON decode failed: {}", error))
}) as RuntimeHostToolCallback
});
set_host_tool_callback(wrapped);
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_string_array_free(value: *mut FfiStringArray) {
if value.is_null() {
return;
}
let value = unsafe { Box::from_raw(value) };
unsafe { free_string_array_parts(value.items, value.len) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_entry_list_free(value: *mut FfiRuntimeEntryDescriptorList) {
if value.is_null() {
return;
}
let value = unsafe { Box::from_raw(value) };
if !value.items.is_null() && value.len > 0 {
let items = unsafe { Vec::from_raw_parts(value.items, value.len, value.len) };
for item in items {
unsafe { free_entry_descriptor(item) };
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_help_list_free(
value: *mut FfiRuntimeSkillHelpDescriptorList,
) {
if value.is_null() {
return;
}
let value = unsafe { Box::from_raw(value) };
if !value.items.is_null() && value.len > 0 {
let items = unsafe { Vec::from_raw_parts(value.items, value.len, value.len) };
for item in items {
unsafe { luaskills_ffi_buffer_free(item.skill_id) };
unsafe { luaskills_ffi_buffer_free(item.skill_name) };
unsafe { luaskills_ffi_buffer_free(item.skill_version) };
unsafe { luaskills_ffi_buffer_free(item.root_name) };
unsafe { luaskills_ffi_buffer_free(item.skill_dir) };
unsafe { free_help_node_descriptor(item.main) };
if !item.flows.is_null() && item.flows_len > 0 {
let flows =
unsafe { Vec::from_raw_parts(item.flows, item.flows_len, item.flows_len) };
for flow in flows {
unsafe { free_help_node_descriptor(flow) };
}
}
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_help_detail_free(value: *mut FfiRuntimeHelpDetail) {
if value.is_null() {
return;
}
let value = unsafe { *Box::from_raw(value) };
unsafe { luaskills_ffi_buffer_free(value.skill_id) };
unsafe { luaskills_ffi_buffer_free(value.skill_name) };
unsafe { luaskills_ffi_buffer_free(value.skill_version) };
unsafe { luaskills_ffi_buffer_free(value.root_name) };
unsafe { luaskills_ffi_buffer_free(value.skill_dir) };
unsafe { luaskills_ffi_buffer_free(value.flow_name) };
unsafe { luaskills_ffi_buffer_free(value.description) };
unsafe { free_string_array_parts(value.related_entries, value.related_entries_len) };
unsafe { luaskills_ffi_buffer_free(value.content_type) };
unsafe { luaskills_ffi_buffer_free(value.content) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_invocation_result_free(
value: *mut FfiRuntimeInvocationResult,
) {
if value.is_null() {
return;
}
let value = unsafe { *Box::from_raw(value) };
unsafe { luaskills_ffi_buffer_free(value.content) };
unsafe { luaskills_ffi_buffer_free(value.template_hint) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_apply_result_free(value: *mut FfiSkillApplyResult) {
if value.is_null() {
return;
}
let value = unsafe { *Box::from_raw(value) };
unsafe { luaskills_ffi_buffer_free(value.skill_id) };
unsafe { luaskills_ffi_buffer_free(value.status) };
unsafe { luaskills_ffi_buffer_free(value.message) };
unsafe { luaskills_ffi_buffer_free(value.version) };
unsafe { luaskills_ffi_buffer_free(value.source_locator) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_uninstall_result_free(
value: *mut FfiSkillUninstallResult,
) {
if value.is_null() {
return;
}
let value = unsafe { *Box::from_raw(value) };
unsafe { luaskills_ffi_buffer_free(value.skill_id) };
unsafe { luaskills_ffi_buffer_free(value.message) };
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_version(
version_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(version_out);
if version_out.is_null() {
return ffi_error_status(error_out, "version_out must not be null");
}
unsafe { *version_out = alloc_owned_buffer_from_string(crate::ffi::FFI_VERSION) };
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_describe(
functions_out: *mut *mut FfiStringArray,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(functions_out);
if functions_out.is_null() {
return ffi_error_status(error_out, "functions_out must not be null");
}
let values = crate::ffi::exported_ffi_function_names();
unsafe { *functions_out = Box::into_raw(Box::new(alloc_string_array(&values))) };
ffi_ok_status(error_out)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_engine_new(
options: *const FfiLuaEngineOptions,
engine_id_out: *mut u64,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_u64(engine_id_out);
if options.is_null() {
return ffi_error_status(error_out, "options must not be null");
}
if engine_id_out.is_null() {
return ffi_error_status(error_out, "engine_id_out must not be null");
}
let options = match parse_engine_options(unsafe { &*options }) {
Ok(options) => options,
Err(error) => return ffi_error_status(error_out, error),
};
match LuaEngine::new(options) {
Ok(engine) => {
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
match ffi_engine_registry().lock() {
Ok(mut registry) => {
registry.insert(engine_id, crate::ffi::FfiEngineSlot::new(engine));
unsafe { *engine_id_out = engine_id };
ffi_ok_status(error_out)
}
Err(_) => ffi_error_status(error_out, "FFI engine registry lock poisoned"),
}
}
Err(error) => ffi_error_status(error_out, error.to_string()),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_engine_free(
engine_id: u64,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
match ffi_engine_registry().lock() {
Ok(mut registry) => {
if registry.remove(&engine_id).is_none() {
ffi_error_status(error_out, format!("FFI engine {} not found", engine_id))
} else {
ffi_ok_status(error_out)
}
}
Err(_) => ffi_error_status(error_out, "FFI engine registry lock poisoned"),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_load_from_dirs(
engine_id: u64,
base_dir: *const c_char,
override_dir: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let base_dir = match parse_required_string(base_dir, "base_dir") {
Ok(value) => PathBuf::from(value),
Err(error) => return ffi_error_status(error_out, error),
};
let override_dir = match parse_optional_string(override_dir, "override_dir") {
Ok(value) => value.map(PathBuf::from),
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.load_from_dirs(&base_dir, override_dir.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_load_from_roots(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.load_from_roots(&skill_roots)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_reload_from_dirs(
engine_id: u64,
base_dir: *const c_char,
override_dir: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let base_dir = match parse_required_string(base_dir, "base_dir") {
Ok(value) => PathBuf::from(value),
Err(error) => return ffi_error_status(error_out, error),
};
let override_dir = match parse_optional_string(override_dir, "override_dir") {
Ok(value) => value.map(PathBuf::from),
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.reload_from_dirs(&base_dir, override_dir.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_reload_from_roots(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.reload_from_roots(&skill_roots)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_list_entries(
engine_id: u64,
authority: i32,
entries_out: *mut *mut FfiRuntimeEntryDescriptorList,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(entries_out);
if entries_out.is_null() {
return ffi_error_status(error_out, "entries_out must not be null");
}
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
Ok(engine.list_entries_for_authority(authority))
}) {
Ok(entries) => {
let mut items: Vec<FfiRuntimeEntryDescriptor> =
entries.iter().map(alloc_entry_descriptor).collect();
let list = FfiRuntimeEntryDescriptorList {
items: items.as_mut_ptr(),
len: items.len(),
};
std::mem::forget(items);
unsafe { *entries_out = Box::into_raw(Box::new(list)) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_list_skill_help(
engine_id: u64,
authority: i32,
help_out: *mut *mut FfiRuntimeSkillHelpDescriptorList,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(help_out);
if help_out.is_null() {
return ffi_error_status(error_out, "help_out must not be null");
}
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
Ok(engine.list_skill_help_for_authority(authority))
}) {
Ok(help_descriptors) => {
let mut items: Vec<FfiRuntimeSkillHelpDescriptor> =
help_descriptors.iter().map(alloc_help_descriptor).collect();
let list = FfiRuntimeSkillHelpDescriptorList {
items: items.as_mut_ptr(),
len: items.len(),
};
std::mem::forget(items);
unsafe { *help_out = Box::into_raw(Box::new(list)) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_render_skill_help_detail(
engine_id: u64,
authority: i32,
skill_id: *const c_char,
flow_name: *const c_char,
request_context_json: FfiBorrowedBuffer,
detail_out: *mut *mut FfiRuntimeHelpDetail,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(detail_out);
if detail_out.is_null() {
return ffi_error_status(error_out, "detail_out must not be null");
}
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let flow_name = match parse_required_string(flow_name, "flow_name") {
Ok(flow_name) => flow_name,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
let request_context =
match parse_request_context_buffer(&request_context_json, "request_context_json") {
Ok(request_context) => request_context,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
engine.render_skill_help_detail_for_authority(
authority,
&skill_id,
&flow_name,
request_context.as_ref(),
)
}) {
Ok(Some(detail)) => {
unsafe { *detail_out = Box::into_raw(Box::new(alloc_help_detail(&detail))) };
ffi_ok_status(error_out)
}
Ok(None) => ffi_error_status(error_out, "Requested help detail was not found"),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_prompt_argument_completions(
engine_id: u64,
authority: i32,
prompt_name: *const c_char,
argument_name: *const c_char,
values_out: *mut *mut FfiStringArray,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(values_out);
if values_out.is_null() {
return ffi_error_status(error_out, "values_out must not be null");
}
let prompt_name = match parse_required_string(prompt_name, "prompt_name") {
Ok(prompt_name) => prompt_name,
Err(error) => return ffi_error_status(error_out, error),
};
let argument_name = match parse_required_string(argument_name, "argument_name") {
Ok(argument_name) => argument_name,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
Ok(engine.prompt_argument_completions_for_authority(
authority,
&prompt_name,
&argument_name,
))
}) {
Ok(Some(values)) => {
unsafe { *values_out = Box::into_raw(Box::new(alloc_string_array(&values))) };
ffi_ok_status(error_out)
}
Ok(None) => {
unsafe { *values_out = Box::into_raw(Box::new(alloc_string_array(&[]))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_is_skill(
engine_id: u64,
authority: i32,
tool_name: *const c_char,
value_out: *mut u8,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_u8(value_out);
if value_out.is_null() {
return ffi_error_status(error_out, "value_out must not be null");
}
let tool_name = match parse_required_string(tool_name, "tool_name") {
Ok(tool_name) => tool_name,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
Ok(engine.is_skill_for_authority(authority, &tool_name))
}) {
Ok(value) => {
unsafe { *value_out = u8::from(value) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_name_for_tool(
engine_id: u64,
authority: i32,
tool_name: *const c_char,
skill_id_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(skill_id_out);
if skill_id_out.is_null() {
return ffi_error_status(error_out, "skill_id_out must not be null");
}
let tool_name = match parse_required_string(tool_name, "tool_name") {
Ok(tool_name) => tool_name,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
Ok(engine.skill_name_for_tool_for_authority(authority, &tool_name))
}) {
Ok(skill_id) => {
unsafe { *skill_id_out = alloc_optional_owned_buffer_from_string(skill_id.as_deref()) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_list(
engine_id: u64,
skill_id: *const c_char,
result_json_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(result_json_out);
if result_json_out.is_null() {
return ffi_error_status(error_out, "result_json_out must not be null");
}
let skill_id = match parse_optional_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
engine.list_skill_config_entries(skill_id.as_deref())
}) {
Ok(entries) => match serde_json::to_string(&entries) {
Ok(result_json) => {
unsafe { *result_json_out = alloc_owned_buffer_from_string(result_json) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(
error_out,
format!("failed to serialize skill config entries: {}", error),
),
},
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_get(
engine_id: u64,
skill_id: *const c_char,
key: *const c_char,
value_out: *mut FfiOwnedBuffer,
found_out: *mut u8,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(value_out);
clear_out_u8(found_out);
if value_out.is_null() {
return ffi_error_status(error_out, "value_out must not be null");
}
if found_out.is_null() {
return ffi_error_status(error_out, "found_out must not be null");
}
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let key = match parse_required_string(key, "key") {
Ok(key) => key,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
engine.get_skill_config_value(&skill_id, &key)
}) {
Ok(value) => {
unsafe {
*found_out = u8::from(value.is_some());
*value_out = alloc_optional_owned_buffer_from_string(value.as_deref());
}
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_set(
engine_id: u64,
skill_id: *const c_char,
key: *const c_char,
value: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let key = match parse_required_string(key, "key") {
Ok(key) => key,
Err(error) => return ffi_error_status(error_out, error),
};
let value = match parse_required_string_allow_empty(value, "value") {
Ok(value) => value,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine.set_skill_config_value(&skill_id, &key, &value)
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_skill_config_delete(
engine_id: u64,
skill_id: *const c_char,
key: *const c_char,
deleted_out: *mut u8,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_u8(deleted_out);
if deleted_out.is_null() {
return ffi_error_status(error_out, "deleted_out must not be null");
}
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let key = match parse_required_string(key, "key") {
Ok(key) => key,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine.delete_skill_config_value(&skill_id, &key)
}) {
Ok(deleted) => {
unsafe { *deleted_out = u8::from(deleted) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_call_skill(
engine_id: u64,
tool_name: *const c_char,
args_json: FfiBorrowedBuffer,
invocation_context: *const FfiLuaInvocationContext,
result_out: *mut *mut FfiRuntimeInvocationResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
let tool_name = match parse_required_string(tool_name, "tool_name") {
Ok(tool_name) => tool_name,
Err(error) => return ffi_error_status(error_out, error),
};
let args = match parse_json_value_or_empty_object_buffer(&args_json, "args_json") {
Ok(args) => args,
Err(error) => return ffi_error_status(error_out, error),
};
let invocation_context = match parse_invocation_context(invocation_context) {
Ok(invocation_context) => invocation_context,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
engine.call_skill(&tool_name, &args, invocation_context.as_ref())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_invocation_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_run_lua(
engine_id: u64,
code: *const c_char,
args_json: FfiBorrowedBuffer,
invocation_context: *const FfiLuaInvocationContext,
result_json_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_buffer(result_json_out);
if result_json_out.is_null() {
return ffi_error_status(error_out, "result_json_out must not be null");
}
let code = match parse_required_string(code, "code") {
Ok(code) => code,
Err(error) => return ffi_error_status(error_out, error),
};
let args = match parse_json_value_or_empty_object_buffer(&args_json, "args_json") {
Ok(args) => args,
Err(error) => return ffi_error_status(error_out, error),
};
let invocation_context = match parse_invocation_context(invocation_context) {
Ok(invocation_context) => invocation_context,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine(engine_id, |engine| {
engine.run_lua(&code, &args, invocation_context.as_ref())
}) {
Ok(result) => match serde_json::to_string(&result) {
Ok(result_json) => {
unsafe { *result_json_out = alloc_owned_buffer_from_string(result_json) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(
error_out,
format!("Failed to serialize Lua result: {}", error),
),
},
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_disable_skill_in_dirs(
engine_id: u64,
base_dir: *const c_char,
override_dir: *const c_char,
skill_id: *const c_char,
reason: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let base_dir = match parse_required_string(base_dir, "base_dir") {
Ok(value) => PathBuf::from(value),
Err(error) => return ffi_error_status(error_out, error),
};
let override_dir = match parse_optional_string(override_dir, "override_dir") {
Ok(value) => value.map(PathBuf::from),
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let reason = match parse_optional_string(reason, "reason") {
Ok(reason) => reason,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.disable_skill(
&base_dir,
override_dir.as_deref(),
&skill_id,
reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_disable_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
skill_id: *const c_char,
reason: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let reason = match parse_optional_string(reason, "reason") {
Ok(reason) => reason,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.disable_skill_in_roots(&skill_roots, &skill_id, reason.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_disable_skill_in_dirs(
engine_id: u64,
base_dir: *const c_char,
override_dir: *const c_char,
authority: i32,
skill_id: *const c_char,
reason: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let base_dir = match parse_required_string(base_dir, "base_dir") {
Ok(value) => PathBuf::from(value),
Err(error) => return ffi_error_status(error_out, error),
};
let override_dir = match parse_optional_string(override_dir, "override_dir") {
Ok(value) => value.map(PathBuf::from),
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let reason = match parse_optional_string(reason, "reason") {
Ok(reason) => reason,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.system_disable_skill(
&base_dir,
override_dir.as_deref(),
authority,
&skill_id,
reason.as_deref(),
)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_disable_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
authority: i32,
skill_id: *const c_char,
reason: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let reason = match parse_optional_string(reason, "reason") {
Ok(reason) => reason,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.system_disable_skill_in_roots(&skill_roots, authority, &skill_id, reason.as_deref())
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_enable_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
skill_id: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.enable_skill(&skill_roots, &skill_id)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_enable_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
authority: i32,
skill_id: *const c_char,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.system_enable_skill(&skill_roots, authority, &skill_id)
.map_err(|error| error.to_string())
}) {
Ok(()) => ffi_ok_status(error_out),
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_uninstall_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
skill_id: *const c_char,
options: *const FfiSkillUninstallOptions,
result_out: *mut *mut FfiSkillUninstallResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let options = parse_uninstall_options(unsafe { options.as_ref() });
match with_engine_mut(engine_id, |engine| {
engine
.uninstall_skill(&skill_roots, &skill_id, &options)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_uninstall_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_uninstall_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
authority: i32,
skill_id: *const c_char,
options: *const FfiSkillUninstallOptions,
result_out: *mut *mut FfiSkillUninstallResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let skill_id = match parse_required_string(skill_id, "skill_id") {
Ok(skill_id) => skill_id,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
let options = parse_uninstall_options(unsafe { options.as_ref() });
match with_engine_mut(engine_id, |engine| {
engine
.system_uninstall_skill(&skill_roots, authority, &skill_id, &options)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_uninstall_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_install_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
request: *const FfiSkillInstallRequest,
result_out: *mut *mut FfiSkillApplyResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
if request.is_null() {
return ffi_error_status(error_out, "request must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let request = match parse_install_request(unsafe { &*request }) {
Ok(request) => request,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.install_skill(&skill_roots, &request)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_apply_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_install_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
authority: i32,
request: *const FfiSkillInstallRequest,
result_out: *mut *mut FfiSkillApplyResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
if request.is_null() {
return ffi_error_status(error_out, "request must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let request = match parse_install_request(unsafe { &*request }) {
Ok(request) => request,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.system_install_skill(&skill_roots, authority, &request)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_apply_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_update_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
request: *const FfiSkillInstallRequest,
result_out: *mut *mut FfiSkillApplyResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
if request.is_null() {
return ffi_error_status(error_out, "request must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let request = match parse_install_request(unsafe { &*request }) {
Ok(request) => request,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.update_skill(&skill_roots, &request)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_apply_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn luaskills_ffi_system_update_skill(
engine_id: u64,
skill_roots: *const FfiRuntimeSkillRoot,
skill_roots_len: usize,
authority: i32,
request: *const FfiSkillInstallRequest,
result_out: *mut *mut FfiSkillApplyResult,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
clear_error_out(error_out);
clear_out_ptr(result_out);
if result_out.is_null() {
return ffi_error_status(error_out, "result_out must not be null");
}
if request.is_null() {
return ffi_error_status(error_out, "request must not be null");
}
let skill_roots = match parse_skill_roots(skill_roots, skill_roots_len) {
Ok(skill_roots) => skill_roots,
Err(error) => return ffi_error_status(error_out, error),
};
let request = match parse_install_request(unsafe { &*request }) {
Ok(request) => request,
Err(error) => return ffi_error_status(error_out, error),
};
let authority = match parse_skill_management_authority(authority, "authority") {
Ok(authority) => authority,
Err(error) => return ffi_error_status(error_out, error),
};
match with_engine_mut(engine_id, |engine| {
engine
.system_update_skill(&skill_roots, authority, &request)
.map_err(|error| error.to_string())
}) {
Ok(result) => {
unsafe { *result_out = Box::into_raw(Box::new(alloc_skill_apply_result(&result))) };
ffi_ok_status(error_out)
}
Err(error) => ffi_error_status(error_out, error),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_help::{
RuntimeHelpDetail as RuntimeHelpDetailModel,
RuntimeHelpNodeDescriptor as RuntimeHelpNodeDescriptorModel,
RuntimeSkillHelpDescriptor as RuntimeSkillHelpDescriptorModel,
};
use crate::{
RuntimeEntryDescriptor as RuntimeEntryDescriptorModel,
RuntimeEntryParameterDescriptor as RuntimeEntryParameterDescriptorModel,
};
fn read_owned_buffer_text(buffer: &FfiOwnedBuffer) -> String {
if buffer.ptr.is_null() || buffer.len == 0 {
return String::new();
}
let bytes = unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) };
String::from_utf8(bytes.to_vec()).expect("buffer text must be utf-8")
}
fn make_borrowed_buffer(text: &str) -> (Vec<u8>, FfiBorrowedBuffer) {
let bytes = text.as_bytes().to_vec();
let buffer = if bytes.is_empty() {
FfiBorrowedBuffer {
ptr: ptr::null(),
len: 0,
}
} else {
FfiBorrowedBuffer {
ptr: bytes.as_ptr(),
len: bytes.len(),
}
};
(bytes, buffer)
}
#[test]
fn buffer_clone_copies_payload_into_owned_storage() {
let input = b"ffi-buffer-demo";
let mut buffer_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
luaskills_ffi_buffer_clone(input.as_ptr(), input.len(), &mut buffer_out, &mut error_out)
};
assert_eq!(status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
assert_eq!(error_out.len, 0);
let copied = unsafe { std::slice::from_raw_parts(buffer_out.ptr, buffer_out.len) };
assert_eq!(copied, input);
unsafe { luaskills_ffi_buffer_free(buffer_out) };
}
#[test]
fn json_provider_callback_bridge_round_trips_owned_buffers() {
unsafe extern "C" fn callback(
request_json: FfiBorrowedBuffer,
_user_data: *mut c_void,
response_out: *mut FfiOwnedBuffer,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
let request_bytes =
unsafe { std::slice::from_raw_parts(request_json.ptr, request_json.len) };
let request_text = std::str::from_utf8(request_bytes).expect("request must be utf-8");
let response_text = format!("{{\"echo\":{}}}", request_text);
unsafe {
*response_out = alloc_owned_buffer_from_bytes(response_text.as_bytes());
*error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
FFI_STATUS_OK
}
let response = invoke_json_provider_callback(callback, 0, "{\"value\":1}")
.expect("callback bridge should succeed");
assert_eq!(response, "{\"echo\":{\"value\":1}}");
}
#[test]
fn entry_list_free_handles_nested_owned_buffers() {
let runtime_entry = RuntimeEntryDescriptorModel {
canonical_name: "demo-entry".to_string(),
skill_id: "demo-skill".to_string(),
local_name: "entry".to_string(),
root_name: "ROOT".to_string(),
skill_dir: "/tmp/demo-skill".to_string(),
description: "Demo entry description".to_string(),
parameters: vec![RuntimeEntryParameterDescriptorModel {
name: "note".to_string(),
param_type: "string".to_string(),
description: "Optional note".to_string(),
required: false,
}],
};
let mut items = vec![alloc_entry_descriptor(&runtime_entry)];
let list = FfiRuntimeEntryDescriptorList {
items: items.as_mut_ptr(),
len: items.len(),
};
std::mem::forget(items);
let list_ptr = Box::into_raw(Box::new(list));
let list_ref = unsafe { &*list_ptr };
assert_eq!(list_ref.len, 1);
let first_entry = unsafe { &*list_ref.items };
assert_eq!(
read_owned_buffer_text(&first_entry.canonical_name),
"demo-entry"
);
assert_eq!(read_owned_buffer_text(&first_entry.skill_id), "demo-skill");
assert_eq!(
read_owned_buffer_text(&first_entry.description),
"Demo entry description"
);
assert_eq!(first_entry.parameters_len, 1);
let first_parameter = unsafe { &*first_entry.parameters };
assert_eq!(read_owned_buffer_text(&first_parameter.name), "note");
assert_eq!(
read_owned_buffer_text(&first_parameter.param_type),
"string"
);
assert_eq!(
read_owned_buffer_text(&first_parameter.description),
"Optional note"
);
assert_eq!(first_parameter.required, 0);
unsafe { luaskills_ffi_entry_list_free(list_ptr) };
}
#[test]
fn help_results_free_handle_nested_owned_buffers() {
let help_detail = RuntimeHelpDetailModel {
skill_id: "demo-skill".to_string(),
skill_name: "Demo Skill".to_string(),
skill_version: "0.1.0".to_string(),
root_name: "ROOT".to_string(),
skill_dir: "/tmp/demo-skill".to_string(),
flow_name: "main".to_string(),
description: "Demo help detail".to_string(),
related_entries: vec!["demo-entry".to_string(), "demo-entry-2".to_string()],
is_main: true,
content_type: "markdown".to_string(),
content: "# Demo".to_string(),
};
let detail_ptr = Box::into_raw(Box::new(alloc_help_detail(&help_detail)));
let detail_ref = unsafe { &*detail_ptr };
assert_eq!(read_owned_buffer_text(&detail_ref.skill_id), "demo-skill");
assert_eq!(read_owned_buffer_text(&detail_ref.flow_name), "main");
assert_eq!(detail_ref.related_entries_len, 2);
let related_entries = unsafe {
std::slice::from_raw_parts(detail_ref.related_entries, detail_ref.related_entries_len)
};
assert_eq!(read_owned_buffer_text(&related_entries[0]), "demo-entry");
assert_eq!(read_owned_buffer_text(&related_entries[1]), "demo-entry-2");
unsafe { luaskills_ffi_help_detail_free(detail_ptr) };
let help_descriptor = RuntimeSkillHelpDescriptorModel {
skill_id: "demo-skill".to_string(),
skill_name: "Demo Skill".to_string(),
skill_version: "0.1.0".to_string(),
root_name: "ROOT".to_string(),
skill_dir: "/tmp/demo-skill".to_string(),
main: RuntimeHelpNodeDescriptorModel {
flow_name: "main".to_string(),
description: "Main help node".to_string(),
related_entries: vec!["demo-entry".to_string()],
is_main: true,
},
flows: vec![RuntimeHelpNodeDescriptorModel {
flow_name: "secondary".to_string(),
description: "Secondary node".to_string(),
related_entries: vec!["demo-entry-2".to_string()],
is_main: false,
}],
};
let mut items = vec![alloc_help_descriptor(&help_descriptor)];
let list = FfiRuntimeSkillHelpDescriptorList {
items: items.as_mut_ptr(),
len: items.len(),
};
std::mem::forget(items);
let list_ptr = Box::into_raw(Box::new(list));
let list_ref = unsafe { &*list_ptr };
assert_eq!(list_ref.len, 1);
let first_help = unsafe { &*list_ref.items };
assert_eq!(read_owned_buffer_text(&first_help.skill_name), "Demo Skill");
assert_eq!(read_owned_buffer_text(&first_help.main.flow_name), "main");
assert_eq!(first_help.main.related_entries_len, 1);
let main_related_entries = unsafe {
std::slice::from_raw_parts(
first_help.main.related_entries,
first_help.main.related_entries_len,
)
};
assert_eq!(
read_owned_buffer_text(&main_related_entries[0]),
"demo-entry"
);
assert_eq!(first_help.flows_len, 1);
let first_flow = unsafe { &*first_help.flows };
assert_eq!(read_owned_buffer_text(&first_flow.flow_name), "secondary");
unsafe { luaskills_ffi_help_list_free(list_ptr) };
}
#[test]
fn standard_ffi_load_and_list_entries_round_trip() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_standard_ffi_entry_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let root_skills_root = temp_root.join("root_skills");
let skills_root = temp_root.join("skills");
let skill_dir = skills_root.join("demo-skill");
std::fs::create_dir_all(&root_skills_root).expect("create root skills root");
std::fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime directory");
std::fs::create_dir_all(temp_root.join("temp")).expect("create temp directory");
std::fs::create_dir_all(temp_root.join("resources")).expect("create resources directory");
std::fs::create_dir_all(temp_root.join("lua_packages"))
.expect("create lua_packages directory");
std::fs::create_dir_all(temp_root.join("bin").join("tools"))
.expect("create tools directory");
std::fs::create_dir_all(temp_root.join("libs")).expect("create libs directory");
std::fs::write(
skill_dir.join("skill.yaml"),
"name: demo-skill\nversion: 0.1.0\nenable: true\nentries:\n - name: ping\n description: Ping entry.\n lua_entry: runtime/ping.lua\n lua_module: demo_skill_ping\n parameters:\n - name: note\n type: string\n description: Optional note.\n required: false\n",
)
.expect("write skill yaml");
std::fs::write(
skill_dir.join("runtime").join("ping.lua"),
"return function(args)\n return 'ok'\nend\n",
)
.expect("write runtime lua");
let temp_dir_text =
CString::new(temp_root.join("temp").display().to_string()).expect("temp_dir cstring");
let resources_dir_text = CString::new(temp_root.join("resources").display().to_string())
.expect("resources_dir cstring");
let lua_packages_dir_text =
CString::new(temp_root.join("lua_packages").display().to_string())
.expect("lua_packages_dir cstring");
let tool_root_dir_text =
CString::new(temp_root.join("bin").join("tools").display().to_string())
.expect("tool_root_dir cstring");
let ffi_root_dir_text =
CString::new(temp_root.join("libs").display().to_string()).expect("ffi_root cstring");
let dependency_dir_name = CString::new("dependencies").expect("dependencies cstring");
let state_dir_name = CString::new("state").expect("state cstring");
let database_dir_name = CString::new("databases").expect("databases cstring");
let root_name = CString::new(" ROOT ").expect("root name cstring");
let skills_root_text =
CString::new(skills_root.display().to_string()).expect("skills root cstring");
let tool_name = CString::new("demo-skill-ping").expect("tool name cstring");
let host_options = FfiLuaRuntimeHostOptions {
temp_dir: temp_dir_text.as_ptr(),
resources_dir: resources_dir_text.as_ptr(),
lua_packages_dir: lua_packages_dir_text.as_ptr(),
host_provided_tool_root: tool_root_dir_text.as_ptr(),
host_provided_lua_root: lua_packages_dir_text.as_ptr(),
host_provided_ffi_root: ffi_root_dir_text.as_ptr(),
download_cache_root: ptr::null(),
dependency_dir_name: dependency_dir_name.as_ptr(),
state_dir_name: state_dir_name.as_ptr(),
database_dir_name: database_dir_name.as_ptr(),
skill_config_file_path: ptr::null(),
allow_network_download: 0,
github_base_url: ptr::null(),
github_api_base_url: ptr::null(),
sqlite_library_path: ptr::null(),
sqlite_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
sqlite_callback_mode: FFI_CALLBACK_MODE_STANDARD,
lancedb_library_path: ptr::null(),
lancedb_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
lancedb_callback_mode: FFI_CALLBACK_MODE_STANDARD,
space_controller_endpoint: ptr::null(),
space_controller_auto_spawn: 0,
space_controller_executable_path: ptr::null(),
space_controller_process_mode: FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE,
cache_config: ptr::null(),
runlua_pool_config: ptr::null(),
reserved_entry_names: ptr::null(),
reserved_entry_names_len: 0,
ignored_skill_ids: ptr::null(),
ignored_skill_ids_len: 0,
enable_skill_management_bridge: 0,
};
let engine_options = FfiLuaEngineOptions {
pool: FfiLuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host: host_options,
};
let mut engine_id = 0_u64;
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let engine_status =
unsafe { luaskills_ffi_engine_new(&engine_options, &mut engine_id, &mut error_out) };
assert_eq!(engine_status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
let ffi_skill_roots = [FfiRuntimeSkillRoot {
name: root_name.as_ptr(),
skills_dir: skills_root_text.as_ptr(),
}];
let mut load_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let load_status = unsafe {
luaskills_ffi_load_from_roots(
engine_id,
ffi_skill_roots.as_ptr(),
ffi_skill_roots.len(),
&mut load_error,
)
};
assert_eq!(load_status, FFI_STATUS_OK);
assert!(load_error.ptr.is_null());
let mut entries_out: *mut FfiRuntimeEntryDescriptorList = ptr::null_mut();
let mut list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let list_status = unsafe {
luaskills_ffi_list_entries(
engine_id,
FFI_SKILL_AUTHORITY_SYSTEM,
&mut entries_out,
&mut list_error,
)
};
assert_eq!(list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
assert!(!entries_out.is_null());
let entries_ref = unsafe { &*entries_out };
assert_eq!(entries_ref.len, 1);
let entry_ref = unsafe { &*entries_ref.items };
assert_eq!(
read_owned_buffer_text(&entry_ref.canonical_name),
"demo-skill-ping"
);
assert_eq!(read_owned_buffer_text(&entry_ref.skill_id), "demo-skill");
assert_eq!(read_owned_buffer_text(&entry_ref.local_name), "ping");
assert_eq!(read_owned_buffer_text(&entry_ref.root_name), " ROOT ");
assert_eq!(
read_owned_buffer_text(&entry_ref.description),
"Ping entry."
);
assert_eq!(entry_ref.parameters_len, 1);
let parameter_ref = unsafe { &*entry_ref.parameters };
assert_eq!(read_owned_buffer_text(¶meter_ref.name), "note");
assert_eq!(read_owned_buffer_text(¶meter_ref.param_type), "string");
assert_eq!(
read_owned_buffer_text(¶meter_ref.description),
"Optional note."
);
assert_eq!(parameter_ref.required, 0);
unsafe { luaskills_ffi_entry_list_free(entries_out) };
entries_out = ptr::null_mut();
list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let delegated_list_status = unsafe {
luaskills_ffi_list_entries(
engine_id,
FFI_SKILL_AUTHORITY_DELEGATED_TOOL,
&mut entries_out,
&mut list_error,
)
};
assert_eq!(delegated_list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
assert!(!entries_out.is_null());
let delegated_entries_ref = unsafe { &*entries_out };
assert_eq!(delegated_entries_ref.len, 0);
unsafe { luaskills_ffi_entry_list_free(entries_out) };
let mut is_skill_out = 0_u8;
let mut is_skill_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let delegated_is_skill_status = unsafe {
luaskills_ffi_is_skill(
engine_id,
FFI_SKILL_AUTHORITY_DELEGATED_TOOL,
tool_name.as_ptr(),
&mut is_skill_out,
&mut is_skill_error,
)
};
assert_eq!(delegated_is_skill_status, FFI_STATUS_OK);
assert!(is_skill_error.ptr.is_null());
assert_eq!(is_skill_out, 0);
let mut skill_id_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut skill_name_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let delegated_skill_name_status = unsafe {
luaskills_ffi_skill_name_for_tool(
engine_id,
FFI_SKILL_AUTHORITY_DELEGATED_TOOL,
tool_name.as_ptr(),
&mut skill_id_out,
&mut skill_name_error,
)
};
assert_eq!(delegated_skill_name_status, FFI_STATUS_OK);
assert!(skill_name_error.ptr.is_null());
assert_eq!(read_owned_buffer_text(&skill_id_out), "");
unsafe { luaskills_ffi_buffer_free(skill_id_out) };
let (call_args_storage, call_args_buffer) = make_borrowed_buffer("{}");
let (run_args_storage, run_args_buffer) = make_borrowed_buffer("{}");
let mut call_result_out: *mut FfiRuntimeInvocationResult = ptr::null_mut();
let mut call_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let call_status = unsafe {
luaskills_ffi_call_skill(
engine_id,
tool_name.as_ptr(),
call_args_buffer,
ptr::null(),
&mut call_result_out,
&mut call_error,
)
};
assert_eq!(call_status, FFI_STATUS_OK);
assert!(call_error.ptr.is_null());
assert!(!call_result_out.is_null());
let call_result_ref = unsafe { &*call_result_out };
assert_eq!(read_owned_buffer_text(&call_result_ref.content), "ok");
unsafe { luaskills_ffi_invocation_result_free(call_result_out) };
let run_code =
CString::new("return vulcan.call('demo-skill-ping', {})").expect("run code cstring");
let mut run_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut run_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let run_status = unsafe {
luaskills_ffi_run_lua(
engine_id,
run_code.as_ptr(),
run_args_buffer,
ptr::null(),
&mut run_out,
&mut run_error,
)
};
assert_eq!(run_status, FFI_STATUS_OK);
assert!(run_error.ptr.is_null());
assert_eq!(read_owned_buffer_text(&run_out), "\"ok\"");
unsafe { luaskills_ffi_buffer_free(run_out) };
let _ = (call_args_storage, run_args_storage);
let prompt_name = CString::new("demo").expect("prompt name cstring");
let argument_name = CString::new("target").expect("argument name cstring");
let mut prompt_values_out: *mut FfiStringArray = ptr::null_mut();
let mut prompt_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let prompt_status = unsafe {
luaskills_ffi_prompt_argument_completions(
engine_id,
FFI_SKILL_AUTHORITY_DELEGATED_TOOL,
prompt_name.as_ptr(),
argument_name.as_ptr(),
&mut prompt_values_out,
&mut prompt_error,
)
};
assert_eq!(prompt_status, FFI_STATUS_OK);
assert!(prompt_error.ptr.is_null());
assert!(!prompt_values_out.is_null());
let prompt_values_ref = unsafe { &*prompt_values_out };
assert_eq!(prompt_values_ref.len, 0);
unsafe { luaskills_ffi_string_array_free(prompt_values_out) };
let mut free_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let free_status = unsafe { luaskills_ffi_engine_free(engine_id, &mut free_error) };
assert_eq!(free_status, FFI_STATUS_OK);
assert!(free_error.ptr.is_null());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn standard_ffi_call_skill_accepts_borrowed_json_buffers() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_standard_ffi_callskill_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let root_skills_root = temp_root.join("root_skills");
let skills_root = temp_root.join("skills");
let skill_dir = skills_root.join("demo-skill");
std::fs::create_dir_all(&root_skills_root).expect("create root skills root");
std::fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime directory");
std::fs::create_dir_all(temp_root.join("temp")).expect("create temp directory");
std::fs::create_dir_all(temp_root.join("resources")).expect("create resources directory");
std::fs::create_dir_all(temp_root.join("lua_packages"))
.expect("create lua_packages directory");
std::fs::create_dir_all(temp_root.join("bin").join("tools"))
.expect("create tools directory");
std::fs::create_dir_all(temp_root.join("libs")).expect("create libs directory");
std::fs::write(
skill_dir.join("skill.yaml"),
"name: demo-skill\nversion: 0.1.0\nenable: true\nentries:\n - name: ping\n description: Ping entry.\n lua_entry: runtime/ping.lua\n lua_module: demo_skill_ping\n parameters:\n - name: note\n type: string\n description: Optional note.\n required: false\n",
)
.expect("write skill yaml");
std::fs::write(
skill_dir.join("runtime").join("ping.lua"),
"return function(args)\n local note = ''\n if type(args) == 'table' and type(args.note) == 'string' then\n note = args.note\n end\n if note ~= '' then\n return 'standard-ffi-test:' .. note\n end\n return 'standard-ffi-test:ok'\nend\n",
)
.expect("write runtime lua");
let temp_dir_text =
CString::new(temp_root.join("temp").display().to_string()).expect("temp_dir cstring");
let resources_dir_text = CString::new(temp_root.join("resources").display().to_string())
.expect("resources_dir cstring");
let lua_packages_dir_text =
CString::new(temp_root.join("lua_packages").display().to_string())
.expect("lua_packages_dir cstring");
let tool_root_dir_text =
CString::new(temp_root.join("bin").join("tools").display().to_string())
.expect("tool_root_dir cstring");
let ffi_root_dir_text =
CString::new(temp_root.join("libs").display().to_string()).expect("ffi_root cstring");
let dependency_dir_name = CString::new("dependencies").expect("dependencies cstring");
let state_dir_name = CString::new("state").expect("state cstring");
let database_dir_name = CString::new("databases").expect("databases cstring");
let root_name = CString::new("ROOT").expect("root name cstring");
let skills_root_text =
CString::new(skills_root.display().to_string()).expect("skills root cstring");
let tool_name = CString::new("demo-skill-ping").expect("tool name cstring");
let host_options = FfiLuaRuntimeHostOptions {
temp_dir: temp_dir_text.as_ptr(),
resources_dir: resources_dir_text.as_ptr(),
lua_packages_dir: lua_packages_dir_text.as_ptr(),
host_provided_tool_root: tool_root_dir_text.as_ptr(),
host_provided_lua_root: lua_packages_dir_text.as_ptr(),
host_provided_ffi_root: ffi_root_dir_text.as_ptr(),
download_cache_root: ptr::null(),
dependency_dir_name: dependency_dir_name.as_ptr(),
state_dir_name: state_dir_name.as_ptr(),
database_dir_name: database_dir_name.as_ptr(),
skill_config_file_path: ptr::null(),
allow_network_download: 0,
github_base_url: ptr::null(),
github_api_base_url: ptr::null(),
sqlite_library_path: ptr::null(),
sqlite_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
sqlite_callback_mode: FFI_CALLBACK_MODE_STANDARD,
lancedb_library_path: ptr::null(),
lancedb_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
lancedb_callback_mode: FFI_CALLBACK_MODE_STANDARD,
space_controller_endpoint: ptr::null(),
space_controller_auto_spawn: 0,
space_controller_executable_path: ptr::null(),
space_controller_process_mode: FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE,
cache_config: ptr::null(),
runlua_pool_config: ptr::null(),
reserved_entry_names: ptr::null(),
reserved_entry_names_len: 0,
ignored_skill_ids: ptr::null(),
ignored_skill_ids_len: 0,
enable_skill_management_bridge: 0,
};
let engine_options = FfiLuaEngineOptions {
pool: FfiLuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host: host_options,
};
let mut engine_id = 0_u64;
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let engine_status =
unsafe { luaskills_ffi_engine_new(&engine_options, &mut engine_id, &mut error_out) };
assert_eq!(engine_status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
let ffi_skill_roots = [FfiRuntimeSkillRoot {
name: root_name.as_ptr(),
skills_dir: skills_root_text.as_ptr(),
}];
let mut load_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let load_status = unsafe {
luaskills_ffi_load_from_roots(
engine_id,
ffi_skill_roots.as_ptr(),
ffi_skill_roots.len(),
&mut load_error,
)
};
assert_eq!(load_status, FFI_STATUS_OK);
assert!(load_error.ptr.is_null());
let (_args_storage, args_buffer) = make_borrowed_buffer(r#"{"note":"ffi"}"#);
let (_request_storage, request_buffer) =
make_borrowed_buffer(r#"{"transport_name":"ffi-test"}"#);
let (_budget_storage, budget_buffer) = make_borrowed_buffer(r#"{"budget":7}"#);
let (_tool_storage, tool_buffer) = make_borrowed_buffer(r#"{"mode":"demo-mode"}"#);
let invocation_context = FfiLuaInvocationContext {
request_context_json: request_buffer,
client_budget_json: budget_buffer,
tool_config_json: tool_buffer,
};
let mut result_out: *mut FfiRuntimeInvocationResult = ptr::null_mut();
let mut call_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let call_status = unsafe {
luaskills_ffi_call_skill(
engine_id,
tool_name.as_ptr(),
args_buffer,
&invocation_context,
&mut result_out,
&mut call_error,
)
};
assert_eq!(call_status, FFI_STATUS_OK);
assert!(call_error.ptr.is_null());
assert!(!result_out.is_null());
let result_ref = unsafe { &*result_out };
assert_eq!(
read_owned_buffer_text(&result_ref.content),
"standard-ffi-test:ffi"
);
assert_eq!(result_ref.content_lines, 1);
unsafe { luaskills_ffi_invocation_result_free(result_out) };
let mut free_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let free_status = unsafe { luaskills_ffi_engine_free(engine_id, &mut free_error) };
assert_eq!(free_status, FFI_STATUS_OK);
assert!(free_error.ptr.is_null());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn standard_ffi_run_lua_accepts_borrowed_json_buffers() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_standard_ffi_runlua_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
std::fs::create_dir_all(temp_root.join("temp")).expect("create temp directory");
std::fs::create_dir_all(temp_root.join("resources")).expect("create resources directory");
std::fs::create_dir_all(temp_root.join("lua_packages"))
.expect("create lua_packages directory");
std::fs::create_dir_all(temp_root.join("bin").join("tools"))
.expect("create tools directory");
std::fs::create_dir_all(temp_root.join("libs")).expect("create libs directory");
let temp_dir_text =
CString::new(temp_root.join("temp").display().to_string()).expect("temp_dir cstring");
let resources_dir_text = CString::new(temp_root.join("resources").display().to_string())
.expect("resources_dir cstring");
let lua_packages_dir_text =
CString::new(temp_root.join("lua_packages").display().to_string())
.expect("lua_packages_dir cstring");
let tool_root_dir_text =
CString::new(temp_root.join("bin").join("tools").display().to_string())
.expect("tool_root_dir cstring");
let ffi_root_dir_text =
CString::new(temp_root.join("libs").display().to_string()).expect("ffi_root cstring");
let dependency_dir_name = CString::new("dependencies").expect("dependencies cstring");
let state_dir_name = CString::new("state").expect("state cstring");
let database_dir_name = CString::new("databases").expect("databases cstring");
let host_options = FfiLuaRuntimeHostOptions {
temp_dir: temp_dir_text.as_ptr(),
resources_dir: resources_dir_text.as_ptr(),
lua_packages_dir: lua_packages_dir_text.as_ptr(),
host_provided_tool_root: tool_root_dir_text.as_ptr(),
host_provided_lua_root: lua_packages_dir_text.as_ptr(),
host_provided_ffi_root: ffi_root_dir_text.as_ptr(),
download_cache_root: ptr::null(),
dependency_dir_name: dependency_dir_name.as_ptr(),
state_dir_name: state_dir_name.as_ptr(),
database_dir_name: database_dir_name.as_ptr(),
skill_config_file_path: ptr::null(),
allow_network_download: 0,
github_base_url: ptr::null(),
github_api_base_url: ptr::null(),
sqlite_library_path: ptr::null(),
sqlite_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
sqlite_callback_mode: FFI_CALLBACK_MODE_STANDARD,
lancedb_library_path: ptr::null(),
lancedb_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
lancedb_callback_mode: FFI_CALLBACK_MODE_STANDARD,
space_controller_endpoint: ptr::null(),
space_controller_auto_spawn: 0,
space_controller_executable_path: ptr::null(),
space_controller_process_mode: FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE,
cache_config: ptr::null(),
runlua_pool_config: ptr::null(),
reserved_entry_names: ptr::null(),
reserved_entry_names_len: 0,
ignored_skill_ids: ptr::null(),
ignored_skill_ids_len: 0,
enable_skill_management_bridge: 0,
};
let engine_options = FfiLuaEngineOptions {
pool: FfiLuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host: host_options,
};
let mut engine_id = 0_u64;
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let engine_status =
unsafe { luaskills_ffi_engine_new(&engine_options, &mut engine_id, &mut error_out) };
assert_eq!(engine_status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
let code =
CString::new("return { note = args.note, transport = vulcan.context.request.transport_name, budget = vulcan.context.client_budget.budget, mode = vulcan.context.tool_config.mode }")
.expect("code cstring");
let (_args_storage, args_buffer) = make_borrowed_buffer(r#"{"note":"demo"}"#);
let (_request_storage, request_buffer) =
make_borrowed_buffer(r#"{"transport_name":"ffi-test"}"#);
let (_budget_storage, budget_buffer) = make_borrowed_buffer(r#"{"budget":7}"#);
let (_tool_storage, tool_buffer) = make_borrowed_buffer(r#"{"mode":"demo-mode"}"#);
let invocation_context = FfiLuaInvocationContext {
request_context_json: request_buffer,
client_budget_json: budget_buffer,
tool_config_json: tool_buffer,
};
let mut result_json_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut run_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let run_status = unsafe {
luaskills_ffi_run_lua(
engine_id,
code.as_ptr(),
args_buffer,
&invocation_context,
&mut result_json_out,
&mut run_error,
)
};
assert_eq!(run_status, FFI_STATUS_OK);
assert!(run_error.ptr.is_null());
let result_json_text = read_owned_buffer_text(&result_json_out);
let result_json: Value =
serde_json::from_str(&result_json_text).expect("run_lua result must be valid json");
assert_eq!(result_json["note"], "demo");
assert_eq!(result_json["transport"], "ffi-test");
assert_eq!(result_json["budget"], 7);
assert_eq!(result_json["mode"], "demo-mode");
unsafe { luaskills_ffi_buffer_free(result_json_out) };
let mut free_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let free_status = unsafe { luaskills_ffi_engine_free(engine_id, &mut free_error) };
assert_eq!(free_status, FFI_STATUS_OK);
assert!(free_error.ptr.is_null());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn standard_ffi_skill_config_round_trip() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_standard_ffi_skill_config_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
std::fs::create_dir_all(temp_root.join("temp")).expect("create temp directory");
std::fs::create_dir_all(temp_root.join("resources")).expect("create resources directory");
std::fs::create_dir_all(temp_root.join("lua_packages"))
.expect("create lua_packages directory");
std::fs::create_dir_all(temp_root.join("bin").join("tools"))
.expect("create tools directory");
std::fs::create_dir_all(temp_root.join("libs")).expect("create libs directory");
let temp_dir_text =
CString::new(temp_root.join("temp").display().to_string()).expect("temp_dir cstring");
let resources_dir_text = CString::new(temp_root.join("resources").display().to_string())
.expect("resources_dir cstring");
let lua_packages_dir_text =
CString::new(temp_root.join("lua_packages").display().to_string())
.expect("lua_packages_dir cstring");
let tool_root_dir_text =
CString::new(temp_root.join("bin").join("tools").display().to_string())
.expect("tool_root_dir cstring");
let ffi_root_dir_text =
CString::new(temp_root.join("libs").display().to_string()).expect("ffi_root cstring");
let dependency_dir_name = CString::new("dependencies").expect("dependencies cstring");
let state_dir_name = CString::new("state").expect("state cstring");
let database_dir_name = CString::new("databases").expect("databases cstring");
let skill_config_file_path = CString::new(
temp_root
.join("config")
.join("skill_config.json")
.display()
.to_string(),
)
.expect("skill config file path cstring");
let skill_id = CString::new("demo-skill").expect("skill_id cstring");
let key = CString::new("api_token").expect("key cstring");
let value = CString::new("sk-standard-ffi").expect("value cstring");
let host_options = FfiLuaRuntimeHostOptions {
temp_dir: temp_dir_text.as_ptr(),
resources_dir: resources_dir_text.as_ptr(),
lua_packages_dir: lua_packages_dir_text.as_ptr(),
host_provided_tool_root: tool_root_dir_text.as_ptr(),
host_provided_lua_root: lua_packages_dir_text.as_ptr(),
host_provided_ffi_root: ffi_root_dir_text.as_ptr(),
download_cache_root: ptr::null(),
dependency_dir_name: dependency_dir_name.as_ptr(),
state_dir_name: state_dir_name.as_ptr(),
database_dir_name: database_dir_name.as_ptr(),
skill_config_file_path: skill_config_file_path.as_ptr(),
allow_network_download: 0,
github_base_url: ptr::null(),
github_api_base_url: ptr::null(),
sqlite_library_path: ptr::null(),
sqlite_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
sqlite_callback_mode: FFI_CALLBACK_MODE_STANDARD,
lancedb_library_path: ptr::null(),
lancedb_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
lancedb_callback_mode: FFI_CALLBACK_MODE_STANDARD,
space_controller_endpoint: ptr::null(),
space_controller_auto_spawn: 0,
space_controller_executable_path: ptr::null(),
space_controller_process_mode: FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE,
cache_config: ptr::null(),
runlua_pool_config: ptr::null(),
reserved_entry_names: ptr::null(),
reserved_entry_names_len: 0,
ignored_skill_ids: ptr::null(),
ignored_skill_ids_len: 0,
enable_skill_management_bridge: 0,
};
let engine_options = FfiLuaEngineOptions {
pool: FfiLuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host: host_options,
};
let mut engine_id = 0_u64;
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let engine_status =
unsafe { luaskills_ffi_engine_new(&engine_options, &mut engine_id, &mut error_out) };
assert_eq!(engine_status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
let mut set_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let set_status = unsafe {
luaskills_ffi_skill_config_set(
engine_id,
skill_id.as_ptr(),
key.as_ptr(),
value.as_ptr(),
&mut set_error,
)
};
assert_eq!(set_status, FFI_STATUS_OK);
assert!(set_error.ptr.is_null());
let mut value_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut found_out = 0_u8;
let mut get_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let get_status = unsafe {
luaskills_ffi_skill_config_get(
engine_id,
skill_id.as_ptr(),
key.as_ptr(),
&mut value_out,
&mut found_out,
&mut get_error,
)
};
assert_eq!(get_status, FFI_STATUS_OK);
assert!(get_error.ptr.is_null());
assert_eq!(found_out, 1);
assert_eq!(read_owned_buffer_text(&value_out), "sk-standard-ffi");
unsafe { luaskills_ffi_buffer_free(value_out) };
let empty_value = CString::new("").expect("empty value cstring");
let mut empty_set_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let empty_set_status = unsafe {
luaskills_ffi_skill_config_set(
engine_id,
skill_id.as_ptr(),
key.as_ptr(),
empty_value.as_ptr(),
&mut empty_set_error,
)
};
assert_eq!(empty_set_status, FFI_STATUS_OK);
assert!(empty_set_error.ptr.is_null());
let mut empty_value_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut empty_found_out = 0_u8;
let mut empty_get_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let empty_get_status = unsafe {
luaskills_ffi_skill_config_get(
engine_id,
skill_id.as_ptr(),
key.as_ptr(),
&mut empty_value_out,
&mut empty_found_out,
&mut empty_get_error,
)
};
assert_eq!(empty_get_status, FFI_STATUS_OK);
assert!(empty_get_error.ptr.is_null());
assert_eq!(empty_found_out, 1);
assert_eq!(read_owned_buffer_text(&empty_value_out), "");
unsafe { luaskills_ffi_buffer_free(empty_value_out) };
let mut list_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let list_status = unsafe {
luaskills_ffi_skill_config_list(
engine_id,
skill_id.as_ptr(),
&mut list_out,
&mut list_error,
)
};
assert_eq!(list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
let list_json: serde_json::Value = serde_json::from_str(&read_owned_buffer_text(&list_out))
.expect("skill config list json should parse");
assert_eq!(list_json.as_array().map(Vec::len), Some(1));
assert_eq!(list_json[0]["skill_id"], "demo-skill");
assert_eq!(list_json[0]["key"], "api_token");
assert_eq!(list_json[0]["value"], "");
unsafe { luaskills_ffi_buffer_free(list_out) };
let mut deleted_out = 0_u8;
let mut delete_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let delete_status = unsafe {
luaskills_ffi_skill_config_delete(
engine_id,
skill_id.as_ptr(),
key.as_ptr(),
&mut deleted_out,
&mut delete_error,
)
};
assert_eq!(delete_status, FFI_STATUS_OK);
assert!(delete_error.ptr.is_null());
assert_eq!(deleted_out, 1);
let mut free_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let free_status = unsafe { luaskills_ffi_engine_free(engine_id, &mut free_error) };
assert_eq!(free_status, FFI_STATUS_OK);
assert!(free_error.ptr.is_null());
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn standard_ffi_disable_and_enable_skill_round_trip() {
let temp_root = std::env::temp_dir().join(format!(
"luaskills_standard_ffi_lifecycle_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let root_skills_root = temp_root.join("root_skills");
let skills_root = temp_root.join("skills");
let skill_dir = skills_root.join("demo-skill");
std::fs::create_dir_all(&root_skills_root).expect("create root skills root");
std::fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime directory");
std::fs::create_dir_all(temp_root.join("temp")).expect("create temp directory");
std::fs::create_dir_all(temp_root.join("resources")).expect("create resources directory");
std::fs::create_dir_all(temp_root.join("lua_packages"))
.expect("create lua_packages directory");
std::fs::create_dir_all(temp_root.join("bin").join("tools"))
.expect("create tools directory");
std::fs::create_dir_all(temp_root.join("libs")).expect("create libs directory");
std::fs::write(
skill_dir.join("skill.yaml"),
"name: demo-skill\nversion: 0.1.0\nenable: true\nentries:\n - name: ping\n description: Ping entry.\n lua_entry: runtime/ping.lua\n lua_module: demo_skill_ping\n parameters:\n - name: note\n type: string\n description: Optional note.\n required: false\n",
)
.expect("write skill yaml");
std::fs::write(
skill_dir.join("runtime").join("ping.lua"),
"return function(args)\n local note = ''\n if type(args) == 'table' and type(args.note) == 'string' then\n note = args.note\n end\n if note ~= '' then\n return 'lifecycle:' .. note\n end\n return 'lifecycle:ok'\nend\n",
)
.expect("write runtime lua");
let temp_dir_text =
CString::new(temp_root.join("temp").display().to_string()).expect("temp_dir cstring");
let resources_dir_text = CString::new(temp_root.join("resources").display().to_string())
.expect("resources_dir cstring");
let lua_packages_dir_text =
CString::new(temp_root.join("lua_packages").display().to_string())
.expect("lua_packages_dir cstring");
let tool_root_dir_text =
CString::new(temp_root.join("bin").join("tools").display().to_string())
.expect("tool_root_dir cstring");
let ffi_root_dir_text =
CString::new(temp_root.join("libs").display().to_string()).expect("ffi_root cstring");
let dependency_dir_name = CString::new("dependencies").expect("dependencies cstring");
let state_dir_name = CString::new("state").expect("state cstring");
let database_dir_name = CString::new("databases").expect("databases cstring");
let root_name = CString::new("ROOT").expect("root name cstring");
let user_name = CString::new("USER").expect("user name cstring");
let root_skills_root_text =
CString::new(root_skills_root.display().to_string()).expect("root skills cstring");
let skills_root_text =
CString::new(skills_root.display().to_string()).expect("skills root cstring");
let skill_id = CString::new("demo-skill").expect("skill_id cstring");
let tool_name = CString::new("demo-skill-ping").expect("tool_name cstring");
let disable_reason = CString::new("maintenance").expect("disable reason cstring");
let host_options = FfiLuaRuntimeHostOptions {
temp_dir: temp_dir_text.as_ptr(),
resources_dir: resources_dir_text.as_ptr(),
lua_packages_dir: lua_packages_dir_text.as_ptr(),
host_provided_tool_root: tool_root_dir_text.as_ptr(),
host_provided_lua_root: lua_packages_dir_text.as_ptr(),
host_provided_ffi_root: ffi_root_dir_text.as_ptr(),
download_cache_root: ptr::null(),
dependency_dir_name: dependency_dir_name.as_ptr(),
state_dir_name: state_dir_name.as_ptr(),
database_dir_name: database_dir_name.as_ptr(),
skill_config_file_path: ptr::null(),
allow_network_download: 0,
github_base_url: ptr::null(),
github_api_base_url: ptr::null(),
sqlite_library_path: ptr::null(),
sqlite_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
sqlite_callback_mode: FFI_CALLBACK_MODE_STANDARD,
lancedb_library_path: ptr::null(),
lancedb_provider_mode: FFI_PROVIDER_MODE_DYNAMIC_LIBRARY,
lancedb_callback_mode: FFI_CALLBACK_MODE_STANDARD,
space_controller_endpoint: ptr::null(),
space_controller_auto_spawn: 0,
space_controller_executable_path: ptr::null(),
space_controller_process_mode: FFI_SPACE_CONTROLLER_PROCESS_MODE_SERVICE,
cache_config: ptr::null(),
runlua_pool_config: ptr::null(),
reserved_entry_names: ptr::null(),
reserved_entry_names_len: 0,
ignored_skill_ids: ptr::null(),
ignored_skill_ids_len: 0,
enable_skill_management_bridge: 0,
};
let engine_options = FfiLuaEngineOptions {
pool: FfiLuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host: host_options,
};
let mut engine_id = 0_u64;
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let engine_status =
unsafe { luaskills_ffi_engine_new(&engine_options, &mut engine_id, &mut error_out) };
assert_eq!(engine_status, FFI_STATUS_OK);
assert!(error_out.ptr.is_null());
let ffi_skill_roots = [
FfiRuntimeSkillRoot {
name: root_name.as_ptr(),
skills_dir: root_skills_root_text.as_ptr(),
},
FfiRuntimeSkillRoot {
name: user_name.as_ptr(),
skills_dir: skills_root_text.as_ptr(),
},
];
let mut load_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let load_status = unsafe {
luaskills_ffi_load_from_roots(
engine_id,
ffi_skill_roots.as_ptr(),
ffi_skill_roots.len(),
&mut load_error,
)
};
assert_eq!(load_status, FFI_STATUS_OK);
assert!(load_error.ptr.is_null());
let mut entries_out: *mut FfiRuntimeEntryDescriptorList = ptr::null_mut();
let mut list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let list_status = unsafe {
luaskills_ffi_list_entries(
engine_id,
FFI_SKILL_AUTHORITY_SYSTEM,
&mut entries_out,
&mut list_error,
)
};
assert_eq!(list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
assert!(!entries_out.is_null());
let entries_ref = unsafe { &*entries_out };
assert_eq!(entries_ref.len, 1);
unsafe { luaskills_ffi_entry_list_free(entries_out) };
let (_before_disable_args_storage, before_disable_args_buffer) =
make_borrowed_buffer(r#"{"note":"before-disable"}"#);
let mut result_out: *mut FfiRuntimeInvocationResult = ptr::null_mut();
let mut call_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let call_status = unsafe {
luaskills_ffi_call_skill(
engine_id,
tool_name.as_ptr(),
before_disable_args_buffer,
ptr::null(),
&mut result_out,
&mut call_error,
)
};
assert_eq!(call_status, FFI_STATUS_OK);
assert!(call_error.ptr.is_null());
let result_ref = unsafe { &*result_out };
assert_eq!(
read_owned_buffer_text(&result_ref.content),
"lifecycle:before-disable"
);
unsafe { luaskills_ffi_invocation_result_free(result_out) };
let mut disable_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let disable_status = unsafe {
luaskills_ffi_disable_skill(
engine_id,
ffi_skill_roots.as_ptr(),
ffi_skill_roots.len(),
skill_id.as_ptr(),
disable_reason.as_ptr(),
&mut disable_error,
)
};
assert_eq!(disable_status, FFI_STATUS_OK);
assert!(disable_error.ptr.is_null());
entries_out = ptr::null_mut();
list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let disabled_list_status = unsafe {
luaskills_ffi_list_entries(
engine_id,
FFI_SKILL_AUTHORITY_SYSTEM,
&mut entries_out,
&mut list_error,
)
};
assert_eq!(disabled_list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
assert!(!entries_out.is_null());
let disabled_entries_ref = unsafe { &*entries_out };
assert_eq!(disabled_entries_ref.len, 0);
unsafe { luaskills_ffi_entry_list_free(entries_out) };
result_out = ptr::null_mut();
call_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let (_disabled_args_storage, disabled_args_buffer) =
make_borrowed_buffer(r#"{"note":"before-disable"}"#);
let disabled_call_status = unsafe {
luaskills_ffi_call_skill(
engine_id,
tool_name.as_ptr(),
disabled_args_buffer,
ptr::null(),
&mut result_out,
&mut call_error,
)
};
assert_ne!(disabled_call_status, FFI_STATUS_OK);
assert!(result_out.is_null());
assert!(!call_error.ptr.is_null());
unsafe { luaskills_ffi_buffer_free(call_error) };
let mut enable_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let enable_status = unsafe {
luaskills_ffi_enable_skill(
engine_id,
ffi_skill_roots.as_ptr(),
ffi_skill_roots.len(),
skill_id.as_ptr(),
&mut enable_error,
)
};
assert_eq!(enable_status, FFI_STATUS_OK);
assert!(enable_error.ptr.is_null());
entries_out = ptr::null_mut();
list_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let enabled_list_status = unsafe {
luaskills_ffi_list_entries(
engine_id,
FFI_SKILL_AUTHORITY_SYSTEM,
&mut entries_out,
&mut list_error,
)
};
assert_eq!(enabled_list_status, FFI_STATUS_OK);
assert!(list_error.ptr.is_null());
assert!(!entries_out.is_null());
let enabled_entries_ref = unsafe { &*entries_out };
assert_eq!(enabled_entries_ref.len, 1);
unsafe { luaskills_ffi_entry_list_free(entries_out) };
let (_enabled_args_storage, enabled_args_buffer) =
make_borrowed_buffer(r#"{"note":"after-enable"}"#);
result_out = ptr::null_mut();
call_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let enabled_call_status = unsafe {
luaskills_ffi_call_skill(
engine_id,
tool_name.as_ptr(),
enabled_args_buffer,
ptr::null(),
&mut result_out,
&mut call_error,
)
};
assert_eq!(enabled_call_status, FFI_STATUS_OK);
assert!(call_error.ptr.is_null());
let enabled_result_ref = unsafe { &*result_out };
assert_eq!(
read_owned_buffer_text(&enabled_result_ref.content),
"lifecycle:after-enable"
);
unsafe { luaskills_ffi_invocation_result_free(result_out) };
let mut free_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let free_status = unsafe { luaskills_ffi_engine_free(engine_id, &mut free_error) };
assert_eq!(free_status, FFI_STATUS_OK);
assert!(free_error.ptr.is_null());
let _ = std::fs::remove_dir_all(&temp_root);
}
}