use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, RegistrationDef, ServiceDef, TypeRef};
use heck::ToSnakeCase;
use std::path::PathBuf;
pub(super) fn gen_service_dart(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let prefix = config.ffi_prefix();
let mut out = String::new();
out.push_str("// Generated by alef. Do not edit by hand.\n\n");
out.push_str("import 'dart:ffi' as ffi;\n");
out.push_str("import 'dart:io' show DynamicLibrary, Platform;\n\n");
out.push_str("/// Callback typedef for C handler registration.\n");
out.push_str("typedef _HandlerCallback = ffi.Pointer<ffi.Char> Function(\n");
out.push_str(" ffi.Pointer<ffi.Void> context,\n");
out.push_str(" ffi.Pointer<ffi.Char> requestJson\n");
out.push_str(");\n\n");
out.push_str("late final ffi.DynamicLibrary _ffiBridge = _loadFFIBridge();\n\n");
out.push_str("ffi.DynamicLibrary _loadFFIBridge() {\n");
out.push_str(" if (Platform.isWindows) {\n");
out.push_str(&format!(
" return DynamicLibrary.open('{}.dll');\n",
config.name.replace('-', "_")
));
out.push_str(" } else if (Platform.isMacOS) {\n");
out.push_str(&format!(
" return DynamicLibrary.open('{}.dylib');\n",
config.name.replace('-', "_")
));
out.push_str(" } else if (Platform.isLinux || Platform.isAndroid) {\n");
out.push_str(&format!(
" return DynamicLibrary.open('lib{}.so');\n",
config.name.replace('-', "_")
));
out.push_str(" } else {\n");
out.push_str(" throw UnsupportedError('Unsupported platform');\n");
out.push_str(" }\n");
out.push_str("}\n\n");
for service in &api.services {
gen_service_class(&mut out, service, api, &prefix);
out.push('\n');
}
out
}
fn gen_service_class(out: &mut String, service: &ServiceDef, api: &ApiSurface, prefix: &str) {
let class_name = &service.name;
let service_snake = service.name.to_snake_case();
let prefix_lower = prefix.to_lowercase();
out.push_str(&format!("/// Service class for {}.\n", class_name));
if !service.doc.is_empty() {
out.push_str(&format!("///\n/// {}\n", service.doc.trim()));
}
out.push_str(&format!("class {class_name} {{\n"));
out.push_str(" late ffi.Pointer<ffi.Void> _owner;\n");
out.push_str(" final Map<ffi.Pointer<ffi.Void>, _HandlerRegistry> _handlers = {};\n");
out.push_str(" int _contextCounter = 0;\n\n");
out.push_str(&format!(" /// Create a new {} instance.\n", class_name));
out.push_str(&format!(
" {class_name}() {{\n \
_owner = _ffiBridge.lookupFunction<\n \
ffi.Pointer<ffi.Void> Function(),\n \
ffi.Pointer<ffi.Void> Function()\n \
>('{prefix_lower}_{service_snake}_new')();\n \
}}\n\n"
));
out.push_str(&format!(" /// Free the {} instance.\n", class_name));
out.push_str(" void dispose() {\n");
out.push_str(&format!(
" _ffiBridge.lookupFunction<void Function(ffi.Pointer<ffi.Void>), void Function(ffi.Pointer<ffi.Void>)>\n \
('{prefix_lower}_{service_snake}_free')(_owner);\n"
));
out.push_str(" _handlers.clear();\n");
out.push_str(" }\n\n");
for reg in &service.registrations {
gen_registration_method(out, service, reg, api, prefix);
out.push('\n');
}
for ep in &service.entrypoints {
gen_entrypoint_method(out, service, ep, prefix);
out.push('\n');
}
out.push_str("}\n");
}
fn gen_registration_method(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
_api: &ApiSurface,
prefix: &str,
) {
let service_snake = service.name.to_snake_case();
let reg_method_snake = reg.method.to_snake_case();
let prefix_lower = prefix.to_lowercase();
let method_name = ®.method;
let mut params = Vec::new();
for meta_param in ®.metadata_params {
let dart_type = typeref_to_dart_type(&meta_param.ty);
if meta_param.optional {
params.push(format!("{}? {}", dart_type, meta_param.name));
} else {
params.push(format!("{} {}", dart_type, meta_param.name));
}
}
params.push("_HandlerCallback? handler".to_string());
let param_sig = params.join(", ");
out.push_str(&format!(" /// Register a handler callback for '{}'.\n", method_name));
if !reg.doc.is_empty() {
out.push_str(&format!(" ///\n /// {}\n", reg.doc.trim()));
}
out.push_str(&format!(" void {method_name}({param_sig}) {{\n"));
out.push_str(" if (handler == null) return;\n\n");
out.push_str(" final contextPtr = ffi.calloc<ffi.Pointer<ffi.Void>>(1);\n");
out.push_str(" contextPtr.value = ffi.Pointer<ffi.Void>.fromAddress(_contextCounter++);\n");
out.push_str(" _handlers[contextPtr.value] = _HandlerRegistry(handler);\n\n");
out.push_str(
" final registerFn = _ffiBridge.lookupFunction<\n \
ffi.Int Function(\n \
ffi.Pointer<ffi.Void>,\n \
ffi.Pointer<ffi.NativeFunction<_HandlerCallback>>,\n \
ffi.Pointer<ffi.Void>",
);
for meta_param in ®.metadata_params {
let c_type = typeref_to_c_ffi_type(&meta_param.ty);
out.push_str(&format!(",\n {c_type}"));
}
out.push_str(
"\n ),\n \
int Function(\n \
ffi.Pointer<ffi.Void>,\n \
ffi.Pointer<ffi.NativeFunction<_HandlerCallback>>,\n \
ffi.Pointer<ffi.Void>",
);
for meta_param in ®.metadata_params {
let dart_type = typeref_to_rust_ffi_type(&meta_param.ty);
out.push_str(&format!(",\n {dart_type}"));
}
out.push_str(&format!(
"\n )\n \
>('{prefix_lower}_{service_snake}_register_{reg_method_snake}');\n\n"
));
out.push_str(" final nativeCallback = ffi.NativeCallable<_HandlerCallback>.isolateLocal(\n");
out.push_str(" (contextPtr, requestJsonPtr) => _handleRequest(requestJsonPtr, handler),\n");
out.push_str(" );\n\n");
out.push_str(" registerFn(\n");
out.push_str(" _owner,\n");
out.push_str(" nativeCallback.nativeFunction,\n");
out.push_str(" contextPtr.value");
for meta_param in ®.metadata_params {
out.push_str(&format!(",\n {}", meta_param.name));
}
out.push_str("\n );\n");
out.push_str(" }\n");
}
fn gen_entrypoint_method(out: &mut String, service: &ServiceDef, ep: &crate::core::ir::EntrypointDef, prefix: &str) {
let service_snake = service.name.to_snake_case();
let ep_name_snake = ep.method.to_snake_case();
let prefix_lower = prefix.to_lowercase();
let ep_method = &ep.method;
let mut params = Vec::new();
for p in &ep.params {
let dart_type = typeref_to_dart_type(&p.ty);
if p.optional {
params.push(format!("{}? {}", dart_type, p.name));
} else {
params.push(format!("{} {}", dart_type, p.name));
}
}
let param_sig = params.join(", ");
let return_type = typeref_to_dart_type(&ep.return_type);
out.push_str(&format!(
" /// {} the service.\n",
if ep.kind == EntrypointKind::Run {
"Run"
} else {
"Finalize"
}
));
if !ep.doc.is_empty() {
out.push_str(&format!(" ///\n /// {}\n", ep.doc.trim()));
}
if ep.is_async {
out.push_str(&format!(" Future<{return_type}> {ep_method}({param_sig}) async {{\n"));
} else {
out.push_str(&format!(" {return_type} {ep_method}({param_sig}) {{\n"));
}
let c_return_type = typeref_to_c_ffi_type(&ep.return_type);
out.push_str(&format!(
" final epFn = _ffiBridge.lookupFunction<\n \
{c_return_type} Function(ffi.Pointer<ffi.Void>"
));
for p in &ep.params {
let c_type = typeref_to_c_ffi_type(&p.ty);
out.push_str(&format!(",\n {c_type}"));
}
out.push_str(&format!(
"\n ),\n \
{return_type} Function(ffi.Pointer<ffi.Void>"
));
for p in &ep.params {
let dart_type = typeref_to_rust_ffi_type(&p.ty);
out.push_str(&format!(",\n {dart_type}"));
}
out.push_str(&format!(
"\n )\n \
>('{prefix_lower}_{service_snake}_ep_{ep_name_snake}');\n\n"
));
out.push_str(" return epFn(_owner");
for p in &ep.params {
out.push_str(&format!(", {}", p.name));
}
out.push_str(");\n");
out.push_str(" }\n");
}
fn typeref_to_dart_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "String".to_owned(),
TypeRef::Char => "String".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "int".to_owned(),
PrimitiveType::U16 => "int".to_owned(),
PrimitiveType::U32 => "int".to_owned(),
PrimitiveType::U64 => "int".to_owned(),
PrimitiveType::I8 => "int".to_owned(),
PrimitiveType::I16 => "int".to_owned(),
PrimitiveType::I32 => "int".to_owned(),
PrimitiveType::I64 => "int".to_owned(),
PrimitiveType::F32 => "double".to_owned(),
PrimitiveType::F64 => "double".to_owned(),
PrimitiveType::Usize => "int".to_owned(),
PrimitiveType::Isize => "int".to_owned(),
}
}
TypeRef::Bytes => "List<int>".to_owned(),
TypeRef::Unit => "void".to_owned(),
TypeRef::Optional(inner) => format!("{}?", typeref_to_dart_type(inner)),
TypeRef::Vec(inner) => format!("List<{}>", typeref_to_dart_type(inner)),
TypeRef::Map(k, v) => format!("Map<{}, {}>", typeref_to_dart_type(k), typeref_to_dart_type(v)),
TypeRef::Named(n) => n.clone(),
TypeRef::Json => "Map<String, dynamic>".to_owned(),
TypeRef::Path => "String".to_owned(),
TypeRef::Duration => "Duration".to_owned(),
}
}
fn typeref_to_c_ffi_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "ffi.Pointer<ffi.Char>".to_owned(),
TypeRef::Char => "ffi.Char".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "ffi.Bool".to_owned(),
PrimitiveType::U8 => "ffi.Uint8".to_owned(),
PrimitiveType::U16 => "ffi.Uint16".to_owned(),
PrimitiveType::U32 => "ffi.Uint32".to_owned(),
PrimitiveType::U64 => "ffi.Uint64".to_owned(),
PrimitiveType::I8 => "ffi.Int8".to_owned(),
PrimitiveType::I16 => "ffi.Int16".to_owned(),
PrimitiveType::I32 => "ffi.Int32".to_owned(),
PrimitiveType::I64 => "ffi.Int64".to_owned(),
PrimitiveType::F32 => "ffi.Float".to_owned(),
PrimitiveType::F64 => "ffi.Double".to_owned(),
PrimitiveType::Usize => "ffi.UintPtr".to_owned(),
PrimitiveType::Isize => "ffi.IntPtr".to_owned(),
}
}
TypeRef::Bytes => "ffi.Pointer<ffi.Uint8>".to_owned(),
TypeRef::Unit => "ffi.Void".to_owned(),
_ => "ffi.Pointer<ffi.Void>".to_owned(),
}
}
fn typeref_to_rust_ffi_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "String".to_owned(),
TypeRef::Char => "String".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "int".to_owned(),
PrimitiveType::U16 => "int".to_owned(),
PrimitiveType::U32 => "int".to_owned(),
PrimitiveType::U64 => "int".to_owned(),
PrimitiveType::I8 => "int".to_owned(),
PrimitiveType::I16 => "int".to_owned(),
PrimitiveType::I32 => "int".to_owned(),
PrimitiveType::I64 => "int".to_owned(),
PrimitiveType::F32 => "double".to_owned(),
PrimitiveType::F64 => "double".to_owned(),
PrimitiveType::Usize => "int".to_owned(),
PrimitiveType::Isize => "int".to_owned(),
}
}
TypeRef::Bytes => "List<int>".to_owned(),
TypeRef::Unit => "void".to_owned(),
TypeRef::Named(n) => n.clone(),
_ => "Object?".to_owned(),
}
}
fn gen_handler_registry(out: &mut String) {
out.push_str("/// Internal handler registry for mapping C context pointers to Dart callbacks.\n");
out.push_str("class _HandlerRegistry {\n");
out.push_str(" final _HandlerCallback callback;\n\n");
out.push_str(" _HandlerRegistry(this.callback);\n");
out.push_str("}\n\n");
out.push_str("/// Delegate function for handling C callback requests.\n");
out.push_str("ffi.Pointer<ffi.Char> _handleRequest(\n");
out.push_str(" ffi.Pointer<ffi.Char> requestJsonPtr,\n");
out.push_str(" _HandlerCallback callback,\n");
out.push_str(") {\n");
out.push_str(" try {\n");
out.push_str(" // Extract the JSON string from the C pointer\n");
out.push_str(" final requestJson = requestJsonPtr.cast<ffi.Utf8>().toDartString();\n\n");
out.push_str(" // Call the Dart handler with the JSON request\n");
out.push_str(" final responseJsonPtr = callback(ffi.nullptr, requestJsonPtr);\n\n");
out.push_str(" // Return the response pointer (the callback is responsible for allocation)\n");
out.push_str(" return responseJsonPtr;\n");
out.push_str(" } catch (e) {\n");
out.push_str(" // On error, return a JSON error response\n");
out.push_str(" // This is a simplified implementation; in production you'd want\n");
out.push_str(" // to properly marshal the error to JSON.\n");
out.push_str(" return ffi.nullptr;\n");
out.push_str(" }\n");
out.push_str("}\n");
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let output_dir = "packages/dart/lib/src".to_string();
let mut service_dart = gen_service_dart(api, config);
gen_handler_registry(&mut service_dart);
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("service.dart"),
content: service_dart,
generated_header: false,
}])
}
#[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_dart_produces_valid_dart() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("import 'dart:ffi'"));
assert!(dart.contains("typedef _HandlerCallback"));
assert!(dart.contains("class TestService"));
assert!(dart.contains("_loadFFIBridge()"));
}
#[test]
fn test_service_class_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("class TestService {"));
assert!(dart.contains("late ffi.Pointer<ffi.Void> _owner;"));
assert!(dart.contains("final Map<ffi.Pointer<ffi.Void>, _HandlerRegistry> _handlers = {};"));
}
#[test]
fn test_constructor_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("TestService()"));
assert!(dart.contains("_ffiBridge.lookupFunction"));
assert!(dart.contains("test_crate_test_service_new"));
}
#[test]
fn test_dispose_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("void dispose()"));
assert!(dart.contains("test_crate_test_service_free"));
assert!(dart.contains("_handlers.clear()"));
}
#[test]
fn test_registration_method_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("void add_handler(String path, _HandlerCallback? handler)"));
assert!(dart.contains("test_crate_test_service_register_add_handler"));
assert!(dart.contains("NativeCallable<_HandlerCallback>.isolateLocal"));
}
#[test]
fn test_entrypoint_method_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("Future<void> run(String addr)"));
assert!(dart.contains("test_crate_test_service_ep_run"));
}
#[test]
fn test_ffi_library_loading_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let dart = gen_service_dart(&api, &config);
assert!(dart.contains("_loadFFIBridge()"));
assert!(dart.contains("Platform.isWindows"));
assert!(dart.contains("Platform.isMacOS"));
assert!(dart.contains("Platform.isLinux"));
}
#[test]
fn test_handler_registry_is_generated() {
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");
let content = &files[0].content;
assert!(content.contains("class _HandlerRegistry"));
assert!(content.contains("final _HandlerCallback callback;"));
assert!(content.contains("_handleRequest"));
}
#[test]
fn 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());
assert!(files[0].path.to_string_lossy().ends_with("service.dart"));
}
#[test]
fn generate_returns_empty_for_no_services() {
let api = ApiSurface::default();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
}