use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{
ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, RegistrationVariant, ServiceDef, TypeRef,
WrapperConstructorArg,
};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}
#[allow(dead_code)]
fn gen_service_h(api: &ApiSurface, crate_name: &str) -> String {
let mut out = String::new();
let header_guard = format!("{}_SERVICE_H", crate_name.to_uppercase().replace('-', "_"));
out.push_str(&format!(
"#ifndef {header_guard}\n\
#define {header_guard}\n\n\
#include <stdint.h>\n\
#include <stdbool.h>\n\n"
));
out.push_str("/* Handler registration callback typedef.\n");
out.push_str(" * Signature: fn(*mut c_void, *const c_char) -> *mut c_char\n");
out.push_str(" * The context pointer is opaque to the C side and is passed through\n");
out.push_str(" * to the callback on each invocation for stateful handlers.\n");
out.push_str(" * The request is a JSON string (null-terminated); the callback returns\n");
out.push_str(" * a newly-allocated JSON response string (null-terminated) that must be\n");
out.push_str(" * freed via {crate_prefix}_free_string(). */\n");
out.push_str("typedef char* (*handler_callback_t)(\n");
out.push_str(" void* context,\n");
out.push_str(" const char* request_json\n");
out.push_str(");\n\n");
for service in &api.services {
let opaque_name = format!("{}Opaque", service.name);
out.push_str(&format!("typedef struct {opaque_name} {opaque_name};\n"));
}
out.push('\n');
for service in &api.services {
gen_service_h_decls(&mut out, service, api, crate_name);
}
out.push_str(&format!("\n#endif /* {header_guard} */\n"));
out
}
#[allow(dead_code)]
fn gen_service_h_decls(out: &mut String, service: &ServiceDef, _api: &ApiSurface, prefix: &str) {
let service_snake = service.name.to_snake_case();
let opaque_name = format!("{}Opaque", service.name);
let prefix_lower = prefix.to_lowercase();
out.push_str(&format!(
"/* Create a new {0} instance. */\n\
{1}{2}* {1}_{3}_new(void);\n\n",
service.name, prefix_lower, opaque_name, service_snake
));
out.push_str(&format!(
"/* Destroy a {0} instance. */\n\
void {1}_{2}_free({1}{3}* ptr);\n\n",
service.name, prefix_lower, service_snake, opaque_name
));
for reg in &service.registrations {
let reg_method_snake = reg.method.to_snake_case();
out.push_str(&format!(
"/* Register a handler for method '{0}'. */\n\
int {1}_{2}_register_{3}(\n \
{1}{4}* owner,\n \
handler_callback_t callback,\n \
void* context",
reg.method, prefix_lower, service_snake, reg_method_snake, opaque_name
));
for meta_param in ®.metadata_params {
let c_type = typeref_to_c_type(&meta_param.ty);
out.push_str(&format!(",\n {} {}", c_type, meta_param.name));
}
out.push_str("\n);\n\n");
}
for ep in &service.entrypoints {
let ep_name_snake = ep.method.to_snake_case();
let return_type = typeref_to_c_type(&ep.return_type);
out.push_str(&format!(
"/* {0} the service. */\n\
{1} {2}_{3}_ep_{4}(\n \
{2}{5}* owner",
if ep.kind == EntrypointKind::Run {
"Run"
} else {
"Finalize"
},
return_type,
prefix_lower,
service_snake,
ep_name_snake,
opaque_name
));
for ep_param in &ep.params {
let c_type = typeref_to_c_type(&ep_param.ty);
out.push_str(&format!(",\n {} {}", c_type, ep_param.name));
}
out.push_str("\n);\n\n");
}
}
fn typeref_to_c_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "const char*".to_owned(),
TypeRef::Char => "char".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "uint8_t".to_owned(),
PrimitiveType::U16 => "uint16_t".to_owned(),
PrimitiveType::U32 => "uint32_t".to_owned(),
PrimitiveType::U64 => "uint64_t".to_owned(),
PrimitiveType::I8 => "int8_t".to_owned(),
PrimitiveType::I16 => "int16_t".to_owned(),
PrimitiveType::I32 => "int32_t".to_owned(),
PrimitiveType::I64 => "int64_t".to_owned(),
PrimitiveType::F32 => "float".to_owned(),
PrimitiveType::F64 => "double".to_owned(),
PrimitiveType::Usize => "uintptr_t".to_owned(),
PrimitiveType::Isize => "intptr_t".to_owned(),
}
}
TypeRef::Bytes => "const uint8_t*".to_owned(),
TypeRef::Unit => "void".to_owned(),
TypeRef::Named(_) => "int32_t".to_owned(), _ => "void*".to_owned(), }
}
fn typeref_to_rust_ffi_type(ty: &TypeRef, core_import: &str) -> String {
match ty {
TypeRef::String => "*const c_char".to_owned(),
TypeRef::Char => "c_char".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "u8".to_owned(),
PrimitiveType::U16 => "u16".to_owned(),
PrimitiveType::U32 => "u32".to_owned(),
PrimitiveType::U64 => "u64".to_owned(),
PrimitiveType::I8 => "i8".to_owned(),
PrimitiveType::I16 => "i16".to_owned(),
PrimitiveType::I32 => "i32".to_owned(),
PrimitiveType::I64 => "i64".to_owned(),
PrimitiveType::F32 => "f32".to_owned(),
PrimitiveType::F64 => "f64".to_owned(),
PrimitiveType::Usize => "usize".to_owned(),
PrimitiveType::Isize => "isize".to_owned(),
}
}
TypeRef::Bytes => "*const u8".to_owned(),
TypeRef::Unit => "()".to_owned(),
TypeRef::Named(n) => {
if core_import.is_empty() {
n.clone()
} else {
format!("{core_import}::{n}")
}
}
_ => "serde_json::Value".to_owned(),
}
}
struct FfiParamBinding {
decl: String,
conversion: String,
arg: String,
pointer: bool,
}
fn ffi_param_binding(p: &crate::core::ir::ParamDef, core_import: &str, api: &ApiSurface) -> FfiParamBinding {
match &p.ty {
TypeRef::String => FfiParamBinding {
decl: format!("{}: *const c_char", p.name),
conversion: format!(
" let {0} = if {0}.is_null() {{\n \
String::new()\n \
}} else {{\n \
// SAFETY: caller guarantees a valid null-terminated C string.\n \
unsafe {{ CStr::from_ptr({0}) }}.to_string_lossy().into_owned()\n \
}};\n",
p.name
),
arg: p.name.clone(),
pointer: true,
},
TypeRef::Named(n) if api.enums.iter().any(|e| e.name == *n) => {
let enum_snake = heck::ToSnakeCase::to_snake_case(n.as_str());
FfiParamBinding {
decl: format!("{}: i32", p.name),
conversion: format!(
" let {0} = {1}::{0}_from_i32({0})\n \
.ok_or_else(|| \"invalid discriminant for {2}\")?;\n",
enum_snake, core_import, n
),
arg: enum_snake,
pointer: false,
}
}
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => FfiParamBinding {
decl: format!("{}: *mut {core_import}::{n}", p.name),
conversion: format!(
" // SAFETY: pointer was produced by the matching opaque `_new`/builder export and is consumed here.\n \
let {0} = unsafe {{ *Box::from_raw({0}) }};\n",
p.name
),
arg: p.name.clone(),
pointer: true,
},
_ => FfiParamBinding {
decl: format!("{}: {}", p.name, typeref_to_rust_ffi_type(&p.ty, core_import)),
conversion: String::new(),
arg: p.name.clone(),
pointer: false,
},
}
}
fn entrypoint_return_representable(ep: &crate::core::ir::EntrypointDef, api: &ApiSurface) -> bool {
match &ep.return_type {
TypeRef::Unit | TypeRef::String | TypeRef::Char | TypeRef::Primitive(_) | TypeRef::Bytes => true,
TypeRef::Named(n) => api.types.iter().any(|t| t.name == *n),
_ => false,
}
}
fn gen_service_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let core_import = config.core_import_name();
let prefix = config.ffi_prefix();
let mut out = String::new();
out.push_str("#![allow(clippy::too_many_arguments, unused_variables, unused_mut)]\n\n");
out.push_str("use std::ffi::{c_char, c_void, CStr, CString};\n");
out.push_str("use std::sync::Arc;\n");
out.push_str("use std::panic;\n\n");
let referenced_contracts: Vec<&HandlerContractDef> = {
let mut names: Vec<&str> = api
.services
.iter()
.flat_map(|s| s.registrations.iter())
.map(|r| r.callback_contract.as_str())
.collect();
names.sort_unstable();
names.dedup();
names.iter().filter_map(|n| find_contract(api, n)).collect()
};
for contract in &referenced_contracts {
gen_handler_bridge(&mut out, contract, &core_import);
}
for service in &api.services {
gen_service_opaque(&mut out, service, &core_import, &prefix);
gen_service_functions(&mut out, service, api, &core_import, &prefix);
}
out
}
fn gen_service_opaque(out: &mut String, service: &ServiceDef, _core_import: &str, prefix: &str) {
let opaque_name = format!("{}Opaque", service.name);
let service_snake = service.name.to_snake_case();
let owner_path = &service.rust_path;
let prefix_lower = prefix.to_lowercase();
out.push_str(&format!(
"/// Opaque handle to a {} service instance.\n\
/// Allocated by {}_{}_new(), freed by {}_{}_free().\n\
#[repr(C)]\n\
pub struct {} {{\n \
inner: Box<{}>,\n\
}}\n\n",
service.name, prefix_lower, service_snake, prefix_lower, service_snake, opaque_name, owner_path
));
out.push_str(&format!(
"/// Allocate a new {} instance.\n\
///\n\
/// # Safety\n\
/// The returned pointer must be freed via {}_{}_free().\n\
/// Never access the pointer after freeing it.\n\
#[no_mangle]\n\
pub extern \"C\" fn {}_{}_new() -> *mut {} {{\n \
let owner = {}::{}();\n \
Box::into_raw(Box::new({} {{\n \
inner: Box::new(owner),\n \
}}))\n\
}}\n\n",
service.name,
prefix_lower,
service_snake,
prefix_lower,
service_snake,
opaque_name,
owner_path,
service.constructor.name,
opaque_name
));
out.push_str(&format!(
"/// Free a {} instance allocated by {}_{}_new().\n\
///\n\
/// # Safety\n\
/// - `ptr` must have been allocated by {}_{}_new().\n\
/// - After this call, `ptr` is invalid and must not be dereferenced.\n\
/// - Calling this twice on the same pointer causes undefined behavior.\n\
#[no_mangle]\n\
pub extern \"C\" fn {}_{}_free(ptr: *mut {}) {{\n \
if !ptr.is_null() {{\n \
// SAFETY: ptr was allocated by into_raw above;\n \
// we are the sole owner and this is the final drop.\n \
unsafe {{\n \
drop(Box::from_raw(ptr));\n \
}}\n \
}}\n\
}}\n\n",
service.name,
prefix_lower,
service_snake,
prefix_lower,
service_snake,
prefix_lower,
service_snake,
opaque_name
));
}
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
let trait_name = &contract.trait_name;
let bridge_name = format!("Ffi{}Bridge", trait_name.to_upper_camel_case());
let dispatch_name = &contract.dispatch.name;
out.push_str(&format!(
"/// FFI handler bridge for the `{trait_name}` contract.\n\
///\n\
/// Wraps a C callback function pointer so it can be called from Rust async code.\n\
/// The callback receives JSON-serialized request and returns JSON response.\n\
pub struct {bridge_name} {{\n \
callback: extern \"C\" fn(*mut c_void, *const c_char) -> *mut c_char,\n \
context: *mut c_void,\n\
}}\n\n"
));
out.push_str(&format!(
"// SAFETY: The C callback function pointer and context pointer are opaque handles.\n\
// The caller is responsible for maintaining the invariant that the context\n\
// pointer remains valid for the lifetime of the bridge. The callback itself\n\
// must be safe to call from async Rust code.\n\
unsafe impl Send for {bridge_name} {{}}\n\
unsafe impl Sync for {bridge_name} {{}}\n\n"
));
let req_type = contract.wire_request_type.as_deref().unwrap_or("serde_json::Value");
let resp_type = contract.wire_response_type.as_deref().unwrap_or("serde_json::Value");
let req_type = if req_type.contains("::") {
req_type.split("::").last().unwrap_or(req_type)
} else {
req_type
};
let resp_type = if resp_type.contains("::") {
resp_type.split("::").last().unwrap_or(resp_type)
} else {
resp_type
};
let extra_param: String = contract
.dispatch_extra_params
.iter()
.map(|p| format!(", {p}"))
.collect();
let wire_name = contract.wire_param_name.as_deref().unwrap_or("request");
let req_path = if req_type == "Value" {
"serde_json::Value".to_string()
} else {
format!("{core_import}::{req_type}")
};
let resp_path = if resp_type == "Value" {
"serde_json::Value".to_string()
} else {
format!("{core_import}::{resp_type}")
};
let box_err = "Box<dyn std::error::Error + Send + Sync>";
let wire_output = format!("Result<{resp_path}, {box_err}>");
let output_type = contract
.dispatch_return_type
.clone()
.unwrap_or_else(|| wire_output.clone());
let tail = match &contract.response_adapter {
Some(adapter) => format!("{adapter}(outcome)"),
None => "outcome".to_string(),
};
out.push_str(&format!(
"impl {core_import}::{trait_name} for {bridge_name} {{\n \
fn {dispatch_name}(\n \
&self{extra_param},\n \
{wire_name}: {req_path},\n \
) -> std::pin::Pin<Box<dyn std::future::Future<Output = {output_type}> + Send + '_>> {{\n \
Box::pin(async move {{\n"
));
out.push_str(&format!(
" \
let outcome: {wire_output} = async move {{\n \
// Serialize request to JSON\n \
let req_json = serde_json::to_string(&{wire_name})\n \
.map_err(|e| Box::new(e) as {box_err})?;\n \
let req_c_str = CString::new(req_json)\n \
.map_err(|e| Box::new(e) as {box_err})?;\n\n \
// Call the C callback on a blocking thread to avoid stalling the async executor.\n \
// Raw pointers are not `Send`, so the context and result pointers cross the\n \
// spawn_blocking boundary as `usize`; the owned `CString` moves in to stay alive.\n \
let callback = self.callback;\n \
let context = self.context as usize;\n \
let resp_addr = tokio::task::spawn_blocking(move || {{\n \
(callback)(context as *mut c_void, req_c_str.as_ptr()) as usize\n \
}})\n \
.await\n \
.map_err(|e| Box::new(e) as {box_err})?;\n \
let resp_ptr = resp_addr as *mut c_char;\n\n \
if resp_ptr.is_null() {{\n \
return Err(\"C callback returned null response\".into());\n \
}}\n\n \
// SAFETY: resp_ptr was returned by the C callback and must be a null-terminated string.\n \
let resp_c_str = unsafe {{ CStr::from_ptr(resp_ptr) }};\n \
let resp_json = resp_c_str.to_string_lossy();\n\n \
// Deserialize response from JSON\n \
let response: {resp_path} = serde_json::from_str(&resp_json)\n \
.map_err(|e| Box::new(e) as {box_err})?;\n\n \
// Free the C-allocated response string. The host allocates it via malloc/strdup\n \
// and hands ownership to us; we release it with the C runtime's free.\n \
// SAFETY: resp_ptr is null-checked above and was produced by the host callback.\n \
unsafe {{\n \
extern \"C\" {{\n \
fn free(ptr: *mut c_void);\n \
}}\n \
free(resp_ptr as *mut c_void);\n \
}}\n\n \
Ok(response)\n \
}}\n \
.await;\n\n \
{tail}\n \
}})\n \
}}\n\
}}\n\n"
));
}
fn gen_service_functions(out: &mut String, service: &ServiceDef, api: &ApiSurface, core_import: &str, prefix: &str) {
let opaque_name = format!("{}Opaque", service.name);
for reg in &service.registrations {
gen_registration_function(out, service, reg, api, core_import, prefix, &opaque_name);
gen_registration_variants(out, service, reg, api, core_import, prefix, &opaque_name);
}
for ep in &service.entrypoints {
gen_entrypoint_function(out, service, ep, api, core_import, prefix, &opaque_name);
}
}
fn gen_registration_function(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
core_import: &str,
prefix: &str,
opaque_name: &str,
) {
let service_snake = service.name.to_snake_case();
let reg_method_snake = reg.method.to_snake_case();
let fn_name = format!(
"{}_{}_register_{}",
prefix.to_lowercase(),
service_snake,
reg_method_snake
);
let contract = find_contract(api, ®.callback_contract).expect("contract not found");
let bridge_name = format!("Ffi{}Bridge", contract.trait_name.to_upper_camel_case());
out.push_str(&format!(
"/// Register a handler callback for method '{0}'.\n\
///\n\
/// # Safety\n\
/// - `owner` must be a valid pointer returned by `{1}_{2}_new()` and not yet freed.\n\
/// - `callback` must be a valid function pointer that remains valid for the lifetime\n\
/// of this service instance.\n\
/// - `context` is an opaque pointer passed to the callback on each invocation.\n\
/// The caller is responsible for keeping it valid.\n\
/// Returns 0 on success, non-zero error code on failure.\n\
#[no_mangle]\n\
pub extern \"C\" fn {fn_name}(\n \
owner: *mut {opaque_name},\n \
callback: extern \"C\" fn(*mut c_void, *const c_char) -> *mut c_char,\n \
context: *mut c_void",
reg.method,
prefix.to_lowercase(),
service_snake
));
let meta_bindings: Vec<FfiParamBinding> = reg
.metadata_params
.iter()
.map(|p| ffi_param_binding(p, core_import, api))
.collect();
for binding in &meta_bindings {
out.push_str(&format!(",\n {}", binding.decl));
}
out.push_str("\n) -> i32 {\n");
out.push_str(" if owner.is_null() {\n");
out.push_str(" return 1; // Error: null pointer\n");
out.push_str(" }\n");
for (meta_param, binding) in reg.metadata_params.iter().zip(&meta_bindings) {
if binding.pointer {
out.push_str(&format!(
" if {}.is_null() {{\n return 1; // Error: null pointer\n }}\n",
meta_param.name
));
}
}
out.push('\n');
for binding in &meta_bindings {
out.push_str(&binding.conversion);
}
if meta_bindings.iter().any(|b| !b.conversion.is_empty()) {
out.push('\n');
}
out.push_str(" let bridge = ");
out.push_str(&bridge_name);
out.push_str(" {\n");
out.push_str(" callback,\n");
out.push_str(" context,\n");
out.push_str(" };\n");
out.push_str(" let handler: Arc<dyn ");
out.push_str(&format!("{}::{}", core_import, contract.trait_name));
out.push_str("> = Arc::new(bridge);\n\n");
let meta_args: String = meta_bindings.iter().map(|b| format!("{}, ", b.arg)).collect();
out.push_str(" // SAFETY: owner was allocated by _new() and is valid until freed.\n");
if reg.error_type.is_some() {
out.push_str(" match unsafe {\n");
out.push_str(" let owner_ref = &mut (*owner).inner;\n");
out.push_str(&format!(" owner_ref.{}({meta_args}handler)\n", reg.method));
out.push_str(" } {\n");
out.push_str(" Ok(_) => 0, // Success\n");
out.push_str(" Err(_) => 1, // Error\n");
out.push_str(" }\n");
} else {
out.push_str(" unsafe {\n");
out.push_str(" let owner_ref = &mut (*owner).inner;\n");
out.push_str(&format!(" owner_ref.{}({meta_args}handler);\n", reg.method));
out.push_str(" }\n");
out.push_str(" 0\n");
}
out.push_str("}\n\n");
}
fn gen_registration_variants(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
core_import: &str,
prefix: &str,
opaque_name: &str,
) {
if reg.variants.is_empty() {
return;
}
let service_snake = service.name.to_snake_case();
let base_fn_name = format!(
"{}_{}_register_{}",
prefix.to_lowercase(),
service_snake,
reg.method.to_snake_case()
);
let new_fn_name = format!("{}_{}_new", prefix.to_lowercase(), service_snake);
let contract = find_contract(api, ®.callback_contract).expect("contract not found");
let bridge_name = format!("Ffi{}Bridge", contract.trait_name.to_upper_camel_case());
for variant in ®.variants {
let wrapper_call = match &variant.wrapper_call {
Some(wc) => wc,
None => continue,
};
gen_registration_variant(
out,
variant,
wrapper_call,
service,
reg,
api,
core_import,
prefix,
opaque_name,
&base_fn_name,
&new_fn_name,
&bridge_name,
contract,
);
}
}
#[allow(clippy::too_many_arguments)]
fn gen_registration_variant(
out: &mut String,
variant: &RegistrationVariant,
wrapper_call: &crate::core::ir::WrapperConstructorCall,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
core_import: &str,
prefix: &str,
opaque_name: &str,
base_fn_name: &str,
new_fn_name: &str,
bridge_name: &str,
contract: &HandlerContractDef,
) {
let service_snake = service.name.to_snake_case();
let variant_fn_name = format!(
"{}_{}_{}",
prefix.to_lowercase(),
service_snake,
variant.name.to_snake_case()
);
let sig_bindings: Vec<FfiParamBinding> = variant
.signature_params
.iter()
.map(|p| ffi_param_binding(p, core_import, api))
.collect();
let default_doc = format!("Variant shortcut `{}` over `{}`.", variant.name, base_fn_name);
let doc = variant.doc.as_deref().unwrap_or(&default_doc);
out.push_str(&format!(
"/// {doc}\n\
///\n\
/// # Safety\n\
/// - `owner` must be a valid pointer returned by `{new_fn_name}()` and not yet freed.\n\
/// - `callback` must be a valid function pointer that remains valid for the lifetime\n\
/// of this service instance.\n\
/// - `context` is an opaque pointer passed to the callback on each invocation.\n\
/// The caller is responsible for keeping it valid.\n\
/// Returns 0 on success, non-zero error code on failure.\n\
#[no_mangle]\n\
pub extern \"C\" fn {variant_fn_name}(\n \
owner: *mut {opaque_name},\n \
callback: extern \"C\" fn(*mut c_void, *const c_char) -> *mut c_char,\n \
context: *mut c_void",
));
for binding in &sig_bindings {
out.push_str(&format!(",\n {}", binding.decl));
}
out.push_str("\n) -> i32 {\n");
out.push_str(" if owner.is_null() {\n");
out.push_str(" return 1; // Error: null pointer\n");
out.push_str(" }\n");
for (param, binding) in variant.signature_params.iter().zip(&sig_bindings) {
if binding.pointer {
out.push_str(&format!(
" if {}.is_null() {{\n return 1; // Error: null pointer\n }}\n",
param.name
));
}
}
out.push('\n');
for binding in &sig_bindings {
out.push_str(&binding.conversion);
}
if sig_bindings.iter().any(|b| !b.conversion.is_empty()) {
out.push('\n');
}
out.push_str(&format!(
" let {} = {}::{}(\n",
wrapper_call.metadata_param, wrapper_call.wrapper_type_path, wrapper_call.constructor_method
));
for arg in &wrapper_call.args {
match arg {
WrapperConstructorArg::Fixed { value_expr, .. } => {
out.push_str(&format!(" {value_expr},\n"));
}
WrapperConstructorArg::Free { param } => {
let binding = sig_bindings
.iter()
.find(|b| {
b.decl.starts_with(&format!("{}: ", param.name)) || b.arg == param.name
})
.map(|b| b.arg.as_str())
.unwrap_or(param.name.as_str());
out.push_str(&format!(" {binding},\n"));
}
}
}
out.push_str(" );\n\n");
out.push_str(&format!(
" let bridge = {bridge_name} {{\n callback,\n context,\n }};\n"
));
out.push_str(&format!(
" let handler: Arc<dyn {core_import}::{}> = Arc::new(bridge);\n\n",
contract.trait_name
));
let meta_args: String = {
let mut args = format!("{}, ", wrapper_call.metadata_param);
for meta_param in ®.metadata_params {
if meta_param.name == wrapper_call.metadata_param {
continue;
}
let is_overridden = variant.overrides.iter().any(|o| o.param_name == meta_param.name);
if is_overridden {
let override_expr = variant
.overrides
.iter()
.find(|o| o.param_name == meta_param.name)
.map(|o| o.value_expr.as_str())
.unwrap_or("");
args.push_str(&format!("{override_expr}, "));
} else {
let binding_arg = sig_bindings
.iter()
.find(|b| b.arg == meta_param.name)
.map(|b| b.arg.as_str())
.unwrap_or(meta_param.name.as_str());
args.push_str(&format!("{binding_arg}, "));
}
}
args
};
out.push_str(" // SAFETY: owner was allocated by _new() and is valid until freed.\n");
if reg.error_type.is_some() {
out.push_str(" match unsafe {\n");
out.push_str(" let owner_ref = &mut (*owner).inner;\n");
out.push_str(&format!(" owner_ref.{}({meta_args}handler)\n", reg.method));
out.push_str(" } {\n");
out.push_str(" Ok(_) => 0, // Success\n");
out.push_str(" Err(_) => 1, // Error\n");
out.push_str(" }\n");
} else {
out.push_str(" unsafe {\n");
out.push_str(" let owner_ref = &mut (*owner).inner;\n");
out.push_str(&format!(" owner_ref.{}({meta_args}handler);\n", reg.method));
out.push_str(" }\n");
out.push_str(" 0\n");
}
out.push_str("}\n\n");
}
fn gen_entrypoint_function(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
api: &ApiSurface,
core_import: &str,
prefix: &str,
opaque_name: &str,
) {
if matches!(ep.kind, EntrypointKind::Finalize) && !entrypoint_return_representable(ep, api) {
return;
}
let service_snake = service.name.to_snake_case();
let ep_name_snake = ep.method.to_snake_case();
let fn_name = format!("{}_{}_ep_{}", prefix.to_lowercase(), service_snake, ep_name_snake);
let returns_opaque = matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n));
let return_type = match &ep.return_type {
TypeRef::Named(n) if returns_opaque => format!("*mut {core_import}::{n}"),
_ => "i32".to_owned(),
};
let null_return = if returns_opaque { "std::ptr::null_mut()" } else { "1" };
out.push_str(&format!(
"/// Run the service entrypoint '{0}'.\n\
///\n\
/// # Safety\n\
/// - `owner` must be a valid pointer returned by `{1}_{2}_new()` and not yet freed.\n\
/// - `owner` is consumed by this call; it must not be used or freed afterwards.\n\
#[no_mangle]\n\
pub extern \"C\" fn {fn_name}(\n \
owner: *mut {opaque_name}",
ep.method,
prefix.to_lowercase(),
service_snake
));
let param_bindings: Vec<FfiParamBinding> = ep
.params
.iter()
.map(|p| ffi_param_binding(p, core_import, api))
.collect();
for binding in ¶m_bindings {
out.push_str(&format!(",\n {}", binding.decl));
}
out.push_str(&format!("\n) -> {return_type} {{\n"));
out.push_str(" if owner.is_null() {\n");
out.push_str(&format!(" return {null_return};\n"));
out.push_str(" }\n");
for (ep_param, binding) in ep.params.iter().zip(¶m_bindings) {
if binding.pointer {
out.push_str(&format!(
" if {}.is_null() {{\n return {null_return};\n }}\n",
ep_param.name
));
}
}
out.push('\n');
for binding in ¶m_bindings {
out.push_str(&binding.conversion);
}
out.push_str(" // SAFETY: owner was allocated by _new() (Box::into_raw) and is consumed here.\n");
out.push_str(" let owner = unsafe { Box::from_raw(owner) };\n");
out.push_str(" let inner = *owner.inner;\n");
let call_args: String = param_bindings
.iter()
.map(|b| b.arg.clone())
.collect::<Vec<_>>()
.join(", ");
if ep.is_async {
out.push_str(" let rt = tokio::runtime::Runtime::new().expect(\"failed to create tokio runtime\");\n");
}
let call = if ep.is_async {
format!("rt.block_on(inner.{}({call_args}))", ep.method)
} else {
format!("inner.{}({call_args})", ep.method)
};
if returns_opaque {
if ep.error_type.is_some() {
out.push_str(&format!(
" match {call} {{\n Ok(value) => Box::into_raw(Box::new(value)),\n Err(_) => std::ptr::null_mut(),\n }}\n"
));
} else {
out.push_str(&format!(" Box::into_raw(Box::new({call}))\n"));
}
} else if ep.error_type.is_some() {
out.push_str(&format!(
" match {call} {{\n Ok(_) => 0,\n Err(_) => 1,\n }}\n"
));
} else {
out.push_str(&format!(" {call};\n 0\n"));
}
out.push_str("}\n\n");
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let output_dir = config
.output_for("ffi")
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("crates/{}-ffi/src/", config.name));
let service_rs = gen_service_rs(api, config);
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("service.rs"),
content: service_rs,
generated_header: true,
}])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
};
fn make_fixture_surface() -> ApiSurface {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: Some("HandlerError".to_owned()),
doc: "Register a request handler.".to_owned(),
variants: vec![],
};
let run_entrypoint = EntrypointDef {
method: "run".to_owned(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![ParamDef {
name: "addr".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Unit,
error_type: Some("IoError".to_owned()),
doc: "Start the service.".to_owned(),
};
let handler_contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "req".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("Response".to_owned()),
is_async: true,
is_static: false,
error_type: None,
doc: "Handle a request.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
},
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("Response".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Handler contract.".to_owned(),
};
ApiSurface {
crate_name: "test_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![run_entrypoint],
doc: "Test service.".to_owned(),
cfg: None,
}],
handler_contracts: vec![handler_contract],
..ApiSurface::default()
}
}
#[test]
fn test_gen_service_rs_produces_valid_rust() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(rs.contains("#[no_mangle]"));
assert!(rs.contains("extern \"C\""));
assert!(rs.contains("TestServiceOpaque"));
assert!(rs.contains("test_service_new"));
assert!(rs.contains("test_service_free"));
assert!(rs.contains("FfiRequestHandlerBridge"));
assert!(rs.contains("Pin<Box<dyn std::future::Future"));
}
#[test]
fn test_handler_bridge_struct_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(rs.contains("struct FfiRequestHandlerBridge"));
assert!(rs.contains("callback: extern \"C\" fn"));
assert!(rs.contains("context: *mut c_void"));
}
#[test]
fn test_opaque_has_constructor_and_destructor() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(rs.contains("pub extern \"C\" fn test_crate_test_service_new()"));
assert!(rs.contains("pub extern \"C\" fn test_crate_test_service_free"));
}
#[test]
fn test_registration_function_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(rs.contains("test_crate_test_service_register_add_handler"));
assert!(rs.contains("extern \"C\" fn(*mut c_void, *const c_char) -> *mut c_char"));
}
#[test]
fn test_entrypoint_function_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(rs.contains("test_crate_test_service_ep_run"));
assert!(rs.contains("tokio::runtime::Runtime"));
}
fn make_surface_with_variant() -> ApiSurface {
use crate::core::ir::{
ParamDef, RegistrationVariant, RegistrationVariantOverride, WrapperConstructorArg, WrapperConstructorCall,
};
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let get_variant = RegistrationVariant {
name: "get".to_owned(),
overrides: vec![RegistrationVariantOverride {
param_name: "method".to_owned(),
value_expr: "my_crate::Method::GET".to_owned(),
}],
wrapper_call: Some(WrapperConstructorCall {
metadata_param: "builder".to_owned(),
wrapper_type_path: "my_crate::RouteBuilder".to_owned(),
wrapper_type_name: "RouteBuilder".to_owned(),
constructor_method: "new".to_owned(),
args: vec![
WrapperConstructorArg::Fixed {
param_name: "method".to_owned(),
value_expr: "my_crate::Method::GET".to_owned(),
},
WrapperConstructorArg::Free {
param: ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
},
],
}),
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: Default::default(),
};
let registration = RegistrationDef {
method: "add_route".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "builder".to_owned(),
ty: TypeRef::Named("RouteBuilder".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: Some("HandlerError".to_owned()),
doc: "Register a route.".to_owned(),
variants: vec![get_variant],
};
let handler_contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "req".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("Response".to_owned()),
is_async: true,
is_static: false,
error_type: None,
doc: "Handle a request.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
},
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("Response".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Handler contract.".to_owned(),
};
ApiSurface {
crate_name: "my_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "App".to_owned(),
rust_path: "my_crate::App".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![],
doc: "App service.".to_owned(),
cfg: None,
}],
handler_contracts: vec![handler_contract],
..ApiSurface::default()
}
}
#[test]
fn test_variant_fn_is_emitted() {
let api = make_surface_with_variant();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(
rs.contains("fn my_crate_app_get("),
"expected variant fn my_crate_app_get not found in:\n{rs}"
);
}
#[test]
fn test_variant_fn_has_no_mangle_and_extern_c() {
let api = make_surface_with_variant();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
let variant_start = rs.find("fn my_crate_app_get(").expect("variant fn not found");
let preamble = &rs[..variant_start];
let preamble_tail = preamble.rsplit("#[no_mangle]").next().unwrap_or(preamble);
assert!(
preamble.contains("#[no_mangle]"),
"#[no_mangle] must precede the variant fn"
);
assert!(
preamble_tail.trim().starts_with("pub extern") || preamble_tail.trim().starts_with("pub unsafe extern"),
"#[no_mangle] must directly precede the extern fn (intervening: `{preamble_tail}`)"
);
}
#[test]
fn test_variant_fn_has_free_param_and_wrapper_construction() {
let api = make_surface_with_variant();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(
rs.contains("path: *const c_char"),
"free param 'path' missing from variant signature"
);
assert!(
rs.contains("my_crate::Method::GET"),
"fixed arg my_crate::Method::GET missing from wrapper construction"
);
assert!(
rs.contains("my_crate::RouteBuilder::new("),
"wrapper constructor call not emitted"
);
}
#[test]
fn test_variant_fn_has_null_check_for_owner() {
let api = make_surface_with_variant();
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
let start = rs.find("fn my_crate_app_get(").expect("variant fn not found");
let body = &rs[start..];
assert!(
body.contains("if owner.is_null()"),
"owner null check missing from variant fn"
);
}
#[test]
fn test_variant_without_wrapper_call_is_not_emitted() {
use crate::core::ir::{ParamDef, RegistrationVariant, RegistrationVariantOverride};
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: String::new(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let plain_variant = RegistrationVariant {
name: "plain".to_owned(),
overrides: vec![RegistrationVariantOverride {
param_name: "path".to_owned(),
value_expr: "\"/fixed\"".to_owned(),
}],
wrapper_call: None,
signature_params: vec![],
doc: None,
style: Default::default(),
};
let registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: None,
doc: String::new(),
variants: vec![plain_variant],
};
let handler_contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
},
optional_methods: vec![],
wire_request_type: None,
wire_response_type: None,
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: String::new(),
};
let api = ApiSurface {
crate_name: "my_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "App".to_owned(),
rust_path: "my_crate::App".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![],
doc: String::new(),
cfg: None,
}],
handler_contracts: vec![handler_contract],
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "my_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rs = gen_service_rs(&api, &config);
assert!(
!rs.contains("fn my_crate_app_plain("),
"plain variant without wrapper_call must not emit a C symbol"
);
}
}