#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
pub use node_app_api::types::{
AppEvent, AppRequest, AppResponse, Capabilities, CapabilityExample, CapabilityRequest,
CapabilityResponse, NodeAppInfo, ProvidedCapability,
};
pub use node_app_api::context::NodeAppContext;
pub use node_app_api::ffi::{FfiResult, NodeAppMetadata, NodeAppVTable};
pub use node_app_api::API_VERSION;
use std::cell::RefCell;
use std::ffi::CString;
use std::sync::atomic::{AtomicPtr, Ordering};
pub const MAX_CAPABILITY_RESPONSE_SIZE: usize = 16 * 1024 * 1024;
#[doc(hidden)]
#[derive(Clone)]
pub struct CurrentTrace {
pub trace_id: String,
pub span_id: String,
pub depth: u8,
}
thread_local! {
#[doc(hidden)]
pub static CURRENT_TRACE: RefCell<Option<CurrentTrace>> = const { RefCell::new(None) };
}
static APP_CONTEXT: AtomicPtr<NodeAppContext> = AtomicPtr::new(std::ptr::null_mut());
pub mod log_level {
pub const TRACE: u32 = 0;
pub const DEBUG: u32 = 1;
pub const INFO: u32 = 2;
pub const WARN: u32 = 3;
pub const ERROR: u32 = 4;
}
pub fn log(level: u32, message: &str) {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return;
}
let c_message = match CString::new(message) {
Ok(s) => s,
Err(_) => return, };
unsafe {
let ctx = &*ctx_ptr;
(ctx.host_log)(ctx.host_data, level, c_message.as_ptr());
}
}
pub fn invoke_capability(request: &CapabilityRequest) -> Result<CapabilityResponse, NodeAppError> {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return Err(NodeAppError::CapabilityError(
"Host context not available".into(),
));
}
let effective_request: std::borrow::Cow<CapabilityRequest> =
if request.trace_id.is_none() {
CURRENT_TRACE.with(|tl| {
if let Some(trace) = tl.borrow().as_ref() {
let injected = CapabilityRequest {
trace_id: Some(trace.trace_id.clone()),
span_id: Some(trace.span_id.clone()),
parent_span_id: None,
trace_depth: Some(trace.depth),
..request.clone()
};
std::borrow::Cow::Owned(injected)
} else {
std::borrow::Cow::Borrowed(request)
}
})
} else {
std::borrow::Cow::Borrowed(request)
};
let request_json = serde_json::to_vec(effective_request.as_ref())?;
unsafe {
let ctx = &*ctx_ptr;
if ctx.host_invoke_capability as usize == 0 {
return Err(NodeAppError::CapabilityError(
"host_invoke_capability callback not available".into(),
));
}
let result = (ctx.host_invoke_capability)(
ctx.host_data,
request_json.as_ptr(),
request_json.len(),
);
if result.success && !result.data.is_null() && result.data_len > 0 {
let response_slice = std::slice::from_raw_parts(result.data, result.data_len);
let response: CapabilityResponse = serde_json::from_slice(response_slice)
.map_err(|e| NodeAppError::CapabilityError(format!("Response deserialization error: {}", e)))?;
Ok(response)
} else if !result.success {
Err(NodeAppError::CapabilityError(format!(
"Host capability invocation failed with error code {}",
result.error_code
)))
} else {
Err(NodeAppError::CapabilityError(
"Empty response from host".into(),
))
}
}
}
pub const MAX_EVENT_NAME_LEN: usize = 256;
pub const MAX_EVENT_DATA_LEN: usize = 64 * 1024;
pub fn publish_event(name: &str, data: &serde_json::Value) -> Result<(), NodeAppError> {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return Err(NodeAppError::EventFailed(
"Host context not available".into(),
));
}
let name_bytes = name.as_bytes();
if name_bytes.len() > MAX_EVENT_NAME_LEN {
return Err(NodeAppError::EventFailed(format!(
"Event name exceeds {} byte limit (got {})",
MAX_EVENT_NAME_LEN,
name_bytes.len()
)));
}
let data_json = serde_json::to_vec(data)?;
if data_json.len() > MAX_EVENT_DATA_LEN {
return Err(NodeAppError::EventFailed(format!(
"Event data exceeds {} byte limit (got {})",
MAX_EVENT_DATA_LEN,
data_json.len()
)));
}
unsafe {
let ctx = &*ctx_ptr;
if ctx.host_publish_event as usize == 0 {
return Err(NodeAppError::EventFailed(
"host_publish_event callback not available".into(),
));
}
let result = (ctx.host_publish_event)(
ctx.host_data,
name_bytes.as_ptr(),
name_bytes.len(),
data_json.as_ptr(),
data_json.len(),
);
if result == 0 {
Ok(())
} else {
Err(NodeAppError::EventFailed(format!(
"host_publish_event returned error code {}",
result
)))
}
}
}
#[macro_export]
macro_rules! log_trace {
($($arg:tt)*) => {
$crate::log($crate::log_level::TRACE, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {
$crate::log($crate::log_level::DEBUG, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {
$crate::log($crate::log_level::INFO, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {
$crate::log($crate::log_level::WARN, &format!($($arg)*))
};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {
$crate::log($crate::log_level::ERROR, &format!($($arg)*))
};
}
#[doc(hidden)]
pub fn __store_context(ctx: *const NodeAppContext) {
APP_CONTEXT.store(ctx as *mut NodeAppContext, Ordering::Release);
}
pub fn get_config(key: &str) -> Option<String> {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return None;
}
let c_key = CString::new(key).ok()?;
unsafe {
let ctx = &*ctx_ptr;
let result = (ctx.host_get_config)(ctx.host_data, c_key.as_ptr());
if result.is_null() {
return None;
}
Some(std::ffi::CStr::from_ptr(result).to_string_lossy().into_owned())
}
}
pub fn set_storage(key: &str, value: &str) {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return;
}
let c_key = match CString::new(key) {
Ok(s) => s,
Err(_) => return,
};
let c_value = match CString::new(value) {
Ok(s) => s,
Err(_) => return,
};
unsafe {
let ctx = &*ctx_ptr;
(ctx.host_set_storage)(ctx.host_data, c_key.as_ptr(), c_value.as_ptr());
}
}
pub fn get_storage(key: &str) -> Option<String> {
let ctx_ptr = APP_CONTEXT.load(Ordering::Acquire);
if ctx_ptr.is_null() {
return None;
}
let c_key = CString::new(key).ok()?;
unsafe {
let ctx = &*ctx_ptr;
let result = (ctx.host_get_storage)(ctx.host_data, c_key.as_ptr());
if result.is_null() {
return None;
}
Some(std::ffi::CStr::from_ptr(result).to_string_lossy().into_owned())
}
}
#[derive(Debug, thiserror::Error)]
pub enum NodeAppError {
#[error("Initialization failed: {0}")]
InitFailed(String),
#[error("Request handling failed: {0}")]
RequestFailed(String),
#[error("Event handling failed: {0}")]
EventFailed(String),
#[error("Shutdown failed: {0}")]
ShutdownFailed(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Capability error: {0}")]
CapabilityError(String),
}
pub trait NodeApp: Default + Send + Sync + 'static {
fn metadata() -> NodeAppInfo;
fn init(&mut self, _ctx: Option<&NodeAppContext>) -> Result<(), NodeAppError> {
Ok(())
}
fn shutdown(&mut self) -> Result<(), NodeAppError> {
Ok(())
}
fn handle_request(&self, _request: AppRequest) -> Result<AppResponse, NodeAppError> {
Ok(AppResponse {
status: 501,
headers: Default::default(),
body: serde_json::json!({"error": "Not implemented"}),
})
}
fn handle_event(&self, _event: AppEvent) -> Result<(), NodeAppError> {
Ok(())
}
fn provided_capabilities() -> Vec<ProvidedCapability> {
Vec::new()
}
fn handle_capability(
&self,
_request: CapabilityRequest,
) -> Result<CapabilityResponse, NodeAppError> {
Err(NodeAppError::CapabilityError(
"Capability handling not implemented".into(),
))
}
}
#[macro_export]
macro_rules! declare_node_app {
($app_type:ty) => {
static APP_INSTANCE: std::sync::OnceLock<std::sync::Mutex<$app_type>> =
std::sync::OnceLock::new();
static VTABLE: std::sync::OnceLock<$crate::NodeAppVTable> =
std::sync::OnceLock::new();
static META_NAME: std::sync::OnceLock<std::ffi::CString> = std::sync::OnceLock::new();
static META_VERSION: std::sync::OnceLock<std::ffi::CString> = std::sync::OnceLock::new();
static META_AUTHOR: std::sync::OnceLock<std::ffi::CString> = std::sync::OnceLock::new();
static META_DESCRIPTION: std::sync::OnceLock<std::ffi::CString> =
std::sync::OnceLock::new();
unsafe extern "C" fn __node_app_init(
ctx: *const std::os::raw::c_void,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let ctx_opt = if ctx.is_null() {
None
} else {
let ctx_typed = ctx as *const $crate::NodeAppContext;
$crate::__store_context(ctx_typed);
Some(unsafe { &*ctx_typed })
};
let app = APP_INSTANCE.get_or_init(|| {
std::sync::Mutex::new(<$app_type>::default())
});
let mut guard = match app.lock() {
Ok(g) => g,
Err(e) => {
eprintln!("[node-app] mutex poisoned in init: {}", e);
return $crate::FfiResult::error(-10);
}
};
match guard.init(ctx_opt) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => {
eprintln!("[node-app] init error: {}", e);
$crate::FfiResult::error(-1)
}
}
})) {
Ok(result) => result,
Err(_) => {
eprintln!("[node-app] panic in init");
$crate::FfiResult::error(-99)
}
}
}
unsafe extern "C" fn __node_app_shutdown() -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if let Some(app) = APP_INSTANCE.get() {
let mut guard = match app.lock() {
Ok(g) => g,
Err(e) => {
eprintln!("[node-app] mutex poisoned in shutdown: {}", e);
return $crate::FfiResult::error(-10);
}
};
match guard.shutdown() {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => {
eprintln!("[node-app] shutdown error: {}", e);
$crate::FfiResult::error(-1)
}
}
} else {
$crate::FfiResult::ok()
}
})) {
Ok(result) => result,
Err(_) => {
eprintln!("[node-app] panic in shutdown");
$crate::FfiResult::error(-99)
}
}
}
unsafe extern "C" fn __node_app_handle_request(
request_json: *const u8,
request_len: usize,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let json_slice =
unsafe { std::slice::from_raw_parts(request_json, request_len) };
let request: $crate::AppRequest = match serde_json::from_slice(json_slice) {
Ok(r) => r,
Err(e) => {
eprintln!("[node-app] request deserialization error: {}", e);
return $crate::FfiResult::error(-2);
}
};
let app = match APP_INSTANCE.get() {
Some(a) => a,
None => return $crate::FfiResult::error(-3),
};
let guard = match app.lock() {
Ok(g) => g,
Err(e) => {
eprintln!("[node-app] mutex poisoned in handle_request: {}", e);
return $crate::FfiResult::error(-10);
}
};
match guard.handle_request(request) {
Ok(response) => match serde_json::to_vec(&response) {
Ok(bytes) => {
let len = bytes.len();
let boxed = bytes.into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut u8;
$crate::FfiResult {
success: true,
error_code: 0,
data: ptr,
data_len: len,
}
}
Err(e) => {
eprintln!("[node-app] response serialization error: {}", e);
$crate::FfiResult::error(-4)
}
},
Err(e) => {
eprintln!("[node-app] handle_request error: {}", e);
$crate::FfiResult::error(-5)
}
}
})) {
Ok(result) => result,
Err(_) => {
eprintln!("[node-app] panic in handle_request");
$crate::FfiResult::error(-99)
}
}
}
unsafe extern "C" fn __node_app_handle_event(
event_json: *const u8,
event_len: usize,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let json_slice =
unsafe { std::slice::from_raw_parts(event_json, event_len) };
let event: $crate::AppEvent = match serde_json::from_slice(json_slice) {
Ok(e) => e,
Err(e) => {
eprintln!("[node-app] event deserialization error: {}", e);
return $crate::FfiResult::error(-2);
}
};
let app = match APP_INSTANCE.get() {
Some(a) => a,
None => return $crate::FfiResult::error(-3),
};
let guard = match app.lock() {
Ok(g) => g,
Err(e) => {
eprintln!("[node-app] mutex poisoned in handle_event: {}", e);
return $crate::FfiResult::error(-10);
}
};
match guard.handle_event(event) {
Ok(()) => $crate::FfiResult::ok(),
Err(e) => {
eprintln!("[node-app] handle_event error: {}", e);
$crate::FfiResult::error(-5)
}
}
})) {
Ok(result) => result,
Err(_) => {
eprintln!("[node-app] panic in handle_event");
$crate::FfiResult::error(-99)
}
}
}
unsafe extern "C" fn __node_app_handle_capability(
request_json: *const u8,
request_len: usize,
) -> $crate::FfiResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let json_slice =
unsafe { std::slice::from_raw_parts(request_json, request_len) };
let request: $crate::CapabilityRequest = match serde_json::from_slice(json_slice) {
Ok(r) => r,
Err(e) => {
eprintln!("[node-app] capability request deserialization error: {}", e);
return $crate::FfiResult::error(-2);
}
};
let app = match APP_INSTANCE.get() {
Some(a) => a,
None => return $crate::FfiResult::error(-3),
};
let guard = match app.lock() {
Ok(g) => g,
Err(e) => {
eprintln!("[node-app] mutex poisoned in handle_capability: {}", e);
return $crate::FfiResult::error(-10);
}
};
$crate::CURRENT_TRACE.with(|tl| {
*tl.borrow_mut() = if let Some(ref trace_id) = request.trace_id {
Some($crate::CurrentTrace {
trace_id: trace_id.clone(),
span_id: request.span_id.clone().unwrap_or_default(),
depth: request.trace_depth.unwrap_or(0),
})
} else {
None
};
});
let cap_result = guard.handle_capability(request);
$crate::CURRENT_TRACE.with(|tl| {
*tl.borrow_mut() = None;
});
match cap_result {
Ok(response) => match serde_json::to_vec(&response) {
Ok(bytes) => {
if bytes.len() > $crate::MAX_CAPABILITY_RESPONSE_SIZE {
eprintln!(
"[node-app] capability response exceeds 16MB limit ({} bytes)",
bytes.len()
);
return $crate::FfiResult::error(-6);
}
let len = bytes.len();
let boxed = bytes.into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut u8;
$crate::FfiResult {
success: true,
error_code: 0,
data: ptr,
data_len: len,
}
}
Err(e) => {
eprintln!("[node-app] capability response serialization error: {}", e);
$crate::FfiResult::error(-4)
}
},
Err(e) => {
eprintln!("[node-app] handle_capability error: {}", e);
$crate::FfiResult::error(-5)
}
}
})) {
Ok(result) => result,
Err(_) => {
eprintln!("[node-app] panic in handle_capability");
$crate::FfiResult::error(-99)
}
}
}
unsafe extern "C" fn __node_app_free(ptr: *mut u8, len: usize) {
if !ptr.is_null() && len > 0 {
let _ = unsafe { Box::from_raw(std::slice::from_raw_parts_mut(ptr, len)) };
}
}
#[no_mangle]
pub unsafe extern "C" fn _node_app_entry(
_ctx: *const std::os::raw::c_void,
) -> *const $crate::NodeAppVTable {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let info = <$app_type as $crate::NodeApp>::metadata();
let caps = info.capability_flags();
let name = META_NAME
.get_or_init(|| std::ffi::CString::new(info.name).unwrap_or_default());
let version = META_VERSION
.get_or_init(|| std::ffi::CString::new(info.version).unwrap_or_default());
let author = META_AUTHOR
.get_or_init(|| std::ffi::CString::new(info.author).unwrap_or_default());
let description = META_DESCRIPTION
.get_or_init(|| std::ffi::CString::new(info.description).unwrap_or_default());
let metadata = $crate::NodeAppMetadata {
api_version: $crate::API_VERSION,
name: name.as_ptr(),
version: version.as_ptr(),
author: author.as_ptr(),
description: description.as_ptr(),
capabilities: caps.bits(),
};
VTABLE.get_or_init(|| $crate::NodeAppVTable {
metadata,
init: __node_app_init,
shutdown: __node_app_shutdown,
handle_request: __node_app_handle_request,
handle_event: __node_app_handle_event,
handle_capability: __node_app_handle_capability,
free: __node_app_free,
}) as *const $crate::NodeAppVTable
})) {
Ok(ptr) => ptr,
Err(_) => {
eprintln!("[node-app] panic in _node_app_entry");
std::ptr::null()
}
}
}
};
}