use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{
ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, RegistrationVariant, 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 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 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 service_param_decl(name: &str, zig_type: &str, multiline: bool) -> String {
if multiline {
format!(",\n {name}: {zig_type}")
} else {
format!(", {name}: {zig_type}")
}
}
fn service_arg(name: &str, ty: &TypeRef, api: &ApiSurface) -> String {
match ty {
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => format!(",\n {name}._handle"),
_ => format!(",\n {name}"),
}
}
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_struct(&mut out, service, api, &prefix, &prefix_lower);
}
out
}
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();
out.push_str(&crate::backends::zig::template_env::render(
"service_struct_open.jinja",
minijinja::context! {
service_name => service_name,
},
));
out.push_str(&crate::backends::zig::template_env::render(
"service_init.jinja",
minijinja::context! {
service_name => service_name,
new_fn => format!("{prefix_lower}_{service_snake}_new"),
},
));
out.push_str(&crate::backends::zig::template_env::render(
"service_deinit.jinja",
minijinja::context! {
service_name => service_name,
free_fn => format!("{prefix_lower}_{service_snake}_free"),
},
));
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");
let mut params_decl = String::new();
for meta_param in ®.metadata_params {
let zig_type = match &meta_param.ty {
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => format!("*{}", n),
ty => typeref_to_zig_type(ty),
};
params_decl.push_str(&service_param_decl(&meta_param.name, &zig_type, true));
}
let mut args = String::new();
for meta_param in ®.metadata_params {
args.push_str(&service_arg(&meta_param.name, &meta_param.ty, api));
}
out.push_str(&crate::backends::zig::template_env::render(
"service_registration_method.jinja",
minijinja::context! {
doc => format!("Register a handler for method '{}'", reg.method),
method_name => reg_method_snake,
service_name => service_name,
params_decl,
c_fn => format!("{prefix_lower}_{service_snake}_register_{reg_method_snake}"),
args,
},
));
for variant in ®.variants {
gen_registration_variant_method(out, service, variant, reg, api, prefix_lower);
}
}
fn gen_registration_variant_method(
out: &mut String,
service: &ServiceDef,
variant: &RegistrationVariant,
_reg: &RegistrationDef,
api: &ApiSurface,
prefix_lower: &str,
) {
let service_name = &service.name;
let service_snake = service.name.to_snake_case();
let variant_name = &variant.name;
let mut params_decl = String::new();
for param in &variant.signature_params {
let zig_type = match ¶m.ty {
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => format!("*{}", n),
ty => typeref_to_zig_type(ty),
};
params_decl.push_str(&service_param_decl(¶m.name, &zig_type, true));
}
let mut args = String::new();
for param in &variant.signature_params {
args.push_str(&service_arg(¶m.name, ¶m.ty, api));
}
let default_doc = format!("Register a handler for {variant_name}");
out.push_str(&crate::backends::zig::template_env::render(
"service_registration_method.jinja",
minijinja::context! {
doc => variant.doc.as_deref().unwrap_or(&default_doc),
method_name => variant_name,
service_name => service_name,
params_decl,
c_fn => format!("{prefix_lower}_{service_snake}_{variant_name}"),
args,
},
));
}
fn gen_entrypoint_method(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
api: &ApiSurface,
prefix_lower: &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 ep_method = &ep.method;
let service_name = &service.name;
let returns_opaque = matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n));
let return_type = if returns_opaque { "?*anyopaque" } else { "c_int" };
let mut params_decl = String::new();
for ep_param in &ep.params {
let zig_type = match &ep_param.ty {
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => "*anyopaque".to_owned(),
ty => typeref_to_zig_type(ty),
};
params_decl.push_str(&service_param_decl(&ep_param.name, &zig_type, false));
}
let mut args = String::new();
for ep_param in &ep.params {
args.push_str(&service_arg(&ep_param.name, &ep_param.ty, api));
}
out.push_str(&crate::backends::zig::template_env::render(
"service_entrypoint_method.jinja",
minijinja::context! {
ep_method => ep_method,
method_name => ep_name_snake,
service_name => service_name,
params_decl,
return_type,
null_return => if returns_opaque { "null" } else { "1" },
returns_opaque,
c_fn => format!("{prefix_lower}_{service_snake}_ep_{ep_name_snake}"),
args,
},
));
}
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,
version: 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: 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,
version: Default::default(),
},
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_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: ?*anyopaque"));
}
#[test]
fn test_c_symbols_are_called_via_cimport() {
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"));
assert!(zig.contains("c.test_crate_test_service_new()"));
assert!(zig.contains("c.test_crate_test_service_free("));
assert!(zig.contains("c.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");
}
#[test]
fn test_registration_variants_emit_shortcut_methods() {
let mut api = make_fixture_surface();
if let Some(reg) = api.services[0].registrations.get_mut(0) {
reg.variants.push(RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: None,
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(),
});
reg.variants.push(RegistrationVariant {
name: "post".to_owned(),
overrides: vec![],
wrapper_call: None,
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a POST handler.".to_owned()),
style: Default::default(),
});
}
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let zig = gen_service_zig(&api, &config);
assert!(zig.contains("pub fn get("), "Expected 'pub fn get(' in:\n{}", zig);
assert!(zig.contains("pub fn post("), "Expected 'pub fn post(' in:\n{}", zig);
assert!(zig.contains("Register a GET handler."));
assert!(zig.contains("Register a POST handler."));
assert!(zig.contains("c.test_crate_test_service_get("));
assert!(zig.contains("c.test_crate_test_service_post("));
}
}