use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef};
use heck::ToSnakeCase;
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)
}
fn typeref_to_zig_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char => "[*:0]const u8".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 => "[*:0]const u8".to_owned(),
TypeRef::Unit => "void".to_owned(),
TypeRef::Named(n) => n.clone(),
TypeRef::Json => "[:0]const u8".to_owned(),
TypeRef::Path => "[:0]const u8".to_owned(),
TypeRef::Duration => "u64".to_owned(),
TypeRef::Optional(_) => "?*anyopaque".to_owned(),
TypeRef::Vec(_) => "[*]anyopaque".to_owned(),
TypeRef::Map(_, _) => "*anyopaque".to_owned(),
}
}
fn gen_service_zig(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let prefix = config.ffi_prefix();
let prefix_lower = prefix.to_lowercase();
let mut out = String::new();
out.push_str("// Generated by alef. Do not edit by hand.\n\n");
out.push_str("const std = @import(\"std\");\n");
out.push_str("const c = @cImport(@cInclude(\"");
out.push_str(&config.ffi_header_name());
out.push_str("\"));\n\n");
for service in &api.services {
gen_service_externs(&mut out, service, api, &prefix_lower);
}
for service in &api.services {
gen_service_struct(&mut out, service, api, &prefix, &prefix_lower);
}
out
}
fn gen_service_externs(out: &mut String, service: &ServiceDef, _api: &ApiSurface, prefix_lower: &str) {
let service_snake = service.name.to_snake_case();
let opaque_name = format!("{}Opaque", service.name);
out.push_str(&format!("// {0} C FFI extern declarations\n", service.name));
out.push_str(&format!(
"extern \"C\" fn {0}_{1}_new() *{2} = undefined;\n\n",
prefix_lower, service_snake, opaque_name
));
out.push_str(&format!(
"extern \"C\" fn {0}_{1}_free(ptr: *{2}) void = undefined;\n\n",
prefix_lower, service_snake, opaque_name
));
for reg in &service.registrations {
let reg_method_snake = reg.method.to_snake_case();
out.push_str(&format!(
"extern \"C\" fn {0}_{1}_register_{2}(\n \
owner: *{3},\n \
callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8,\n \
context: *anyopaque",
prefix_lower, service_snake, reg_method_snake, opaque_name
));
for meta_param in ®.metadata_params {
let zig_type = typeref_to_zig_type(&meta_param.ty);
out.push_str(&format!(",\n {} {}", zig_type, meta_param.name));
}
out.push_str("\n) c_int = undefined;\n\n");
}
for ep in &service.entrypoints {
let ep_name_snake = ep.method.to_snake_case();
let return_type = typeref_to_zig_type(&ep.return_type);
out.push_str(&format!(
"extern \"C\" fn {0}_{1}_ep_{2}(\n \
owner: *{3}",
prefix_lower, service_snake, ep_name_snake, opaque_name
));
for ep_param in &ep.params {
let zig_type = typeref_to_zig_type(&ep_param.ty);
out.push_str(&format!(",\n {} {}", zig_type, ep_param.name));
}
out.push_str(&format!("\n) {return_type} = undefined;\n\n"));
}
}
fn gen_service_struct(out: &mut String, service: &ServiceDef, api: &ApiSurface, _prefix: &str, prefix_lower: &str) {
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let opaque_name = format!("{}Opaque", service_name);
out.push_str(&format!(
"/// Zig wrapper for the {0} service.\n\
/// Owns an opaque C pointer to the service instance.\n\
pub const {0} = struct {{\n \
owner: *{1},\n\n",
service_name, opaque_name
));
out.push_str(&format!(
" /// Create a new {0} instance.\n \
pub fn init() {0} {{\n \
return .{{\n \
.owner = {1}_{2}_new(),\n \
}};\n \
}}\n\n",
service_name, prefix_lower, service_snake
));
out.push_str(&format!(
" /// Free the {0} instance.\n \
pub fn deinit(self: *{0}) void {{\n \
if (self.owner != null) {{\n \
{1}_{2}_free(self.owner);\n \
self.owner = null;\n \
}}\n \
}}\n\n",
service_name, prefix_lower, service_snake
));
for reg in &service.registrations {
gen_registration_method(out, service, reg, api, prefix_lower);
}
for ep in &service.entrypoints {
gen_entrypoint_method(out, service, ep, api, prefix_lower);
}
out.push_str("};\n\n");
}
fn gen_registration_method(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
prefix_lower: &str,
) {
let service_snake = service.name.to_snake_case();
let reg_method_snake = reg.method.to_snake_case();
let service_name = &service.name;
let _contract = find_contract(api, ®.callback_contract).expect("contract not found");
out.push_str(&format!(
" /// Register a handler for method '{0}'.\n \
pub fn {1}(\n \
self: *{2},\n \
callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8,\n \
context: *anyopaque",
reg.method, reg_method_snake, service_name
));
for meta_param in ®.metadata_params {
let zig_type = typeref_to_zig_type(&meta_param.ty);
out.push_str(&format!(",\n {}: {}", meta_param.name, zig_type));
}
out.push_str(") c_int {\n");
out.push_str(" if (self.owner == null) {\n");
out.push_str(" return 1; // Error: null pointer\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" return {}_{}_register_{}(\n \
self.owner,\n \
callback,\n \
context",
prefix_lower, service_snake, reg_method_snake
));
for meta_param in ®.metadata_params {
out.push_str(&format!(",\n {}", meta_param.name));
}
out.push_str("\n );\n");
out.push_str(" }\n\n");
}
fn gen_entrypoint_method(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
_api: &ApiSurface,
prefix_lower: &str,
) {
let service_snake = service.name.to_snake_case();
let ep_name_snake = ep.method.to_snake_case();
let ep_method = &ep.method;
let service_name = &service.name;
let return_type = typeref_to_zig_type(&ep.return_type);
out.push_str(&format!(
" /// Run the service entrypoint '{0}'.\n \
pub fn {1}(self: *{2}",
ep_method, ep_name_snake, service_name
));
for ep_param in &ep.params {
let zig_type = typeref_to_zig_type(&ep_param.ty);
out.push_str(&format!(", {}: {}", ep_param.name, zig_type));
}
out.push_str(&format!(") {return_type} {{\n"));
out.push_str(" if (self.owner == null) {\n");
match return_type.as_str() {
"void" => out.push_str(" return;\n"),
_ => out.push_str(" return 0;\n"),
}
out.push_str(" }\n\n");
out.push_str(&format!(
" return {}_{}_ep_{}(\n \
self.owner",
prefix_lower, service_snake, ep_name_snake
));
for ep_param in &ep.params {
out.push_str(&format!(",\n {}", ep_param.name));
}
out.push_str("\n );\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 module_name = config.name.replace('-', "_");
let zig_code = gen_service_zig(api, config);
let dir = crate::core::config::resolve_output_dir(None, &config.name, "packages/zig/src");
let path = PathBuf::from(dir).join(format!("{}_service.zig", module_name));
Ok(vec![GeneratedFile {
path,
content: zig_code,
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(),
};
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()),
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_zig_produces_valid_zig() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("const std = @import(\"std\")"));
assert!(zig.contains("const c = @cImport"));
assert!(zig.contains("TestService"));
assert!(zig.contains("pub fn init()"));
assert!(
zig.contains("pub fn deinit("),
"Expected 'pub fn deinit(' but got:\n{}",
zig
);
}
#[test]
fn test_service_struct_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("pub const TestService = struct"));
assert!(zig.contains("owner: *TestServiceOpaque"));
}
#[test]
fn test_extern_declarations_are_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("extern \"C\" fn test_crate_test_service_new()"));
assert!(zig.contains("extern \"C\" fn test_crate_test_service_free("));
assert!(zig.contains("extern \"C\" fn test_crate_test_service_register_add_handler("));
assert!(zig.contains("fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8"));
}
#[test]
fn test_registration_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("pub fn add_handler("));
assert!(zig.contains("self: *TestService"));
assert!(zig.contains("callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8"));
}
#[test]
fn test_metadata_parameters_included_in_registration() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(
zig.contains("path: [*:0]const u8"),
"Expected 'path: [*:0]const u8' but got:\n{}",
zig
);
}
#[test]
fn test_entrypoint_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("pub fn run("));
assert!(zig.contains("addr: [*:0]const u8"));
}
#[test]
fn test_generate_returns_one_file_for_non_empty_services() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert_eq!(files.len(), 1, "expected 1 generated file, got {}", files.len());
let file = &files[0];
assert!(file.path.to_string_lossy().ends_with("_service.zig"));
}
#[test]
fn test_generate_returns_empty_for_no_services() {
let surface = ApiSurface::default();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&surface, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
}