use crate::codegen::naming::{csharp_type_name, to_csharp_name};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointDef, ServiceDef, TypeRef};
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
fn entrypoint_return_representable(ep: &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 && t.is_opaque),
_ => false,
}
}
fn csharp_type_for_metadata(ty: &TypeRef, api: &ApiSurface) -> String {
match ty {
TypeRef::String | TypeRef::Char => "string".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "byte".to_owned(),
PrimitiveType::U16 => "ushort".to_owned(),
PrimitiveType::U32 => "uint".to_owned(),
PrimitiveType::U64 => "ulong".to_owned(),
PrimitiveType::I8 => "sbyte".to_owned(),
PrimitiveType::I16 => "short".to_owned(),
PrimitiveType::I32 => "int".to_owned(),
PrimitiveType::I64 => "long".to_owned(),
PrimitiveType::F32 => "float".to_owned(),
PrimitiveType::F64 => "double".to_owned(),
PrimitiveType::Usize => "nuint".to_owned(),
PrimitiveType::Isize => "nint".to_owned(),
}
}
TypeRef::Bytes => "byte[]".to_owned(),
TypeRef::Unit => "void".to_owned(),
TypeRef::Named(name) => {
if api.types.iter().any(|t| t.name == *name && t.is_opaque) {
csharp_type_name(name)
} else {
"string".to_owned() }
}
_ => "string".to_owned(), }
}
fn gen_service_cs(api: &ApiSurface, service: &ServiceDef, namespace: &str, prefix: &str) -> String {
let mut out = String::new();
out.push_str("// Auto-generated by alef — DO NOT EDIT\n");
out.push_str("#nullable enable\n\n");
out.push_str(&format!("namespace {namespace} {{\n\n"));
out.push_str("using System;\n");
out.push_str("using System.Collections.Generic;\n");
out.push_str("using System.Linq;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
let class_name = to_csharp_name(&service.name);
out.push_str("/// <summary>\n");
out.push_str(&format!("/// Service wrapper for {}.\n", service.name));
out.push_str("/// </summary>\n");
out.push_str(&format!("public class {class_name} {{\n\n"));
out.push_str(" private IntPtr _handle;\n");
out.push_str(" private static readonly Dictionary<IntPtr, GCHandle> _registeredCallbacks = new();\n");
out.push_str(" private static readonly NativeMethods.HandlerCallback _handlerCallback = HandlerTrampoline;\n\n");
{
let ctor = &service.constructor;
out.push_str(" /// <summary>\n");
out.push_str(&format!(" /// Create a new {}.\n", service.name));
out.push_str(" /// </summary>\n");
if ctor.params.is_empty() {
out.push_str(&format!(" public {class_name}() {{\n"));
out.push_str(&format!(
" _handle = NativeMethods.{}_{}_new();\n",
prefix.to_lowercase(),
service.name.to_snake_case()
));
out.push_str(" }\n\n");
} else {
out.push_str(&format!(" public {class_name}("));
for (i, param) in ctor.params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let ty = csharp_type_for_metadata(¶m.ty, api);
let name = param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", ty, name));
}
out.push_str(") {\n");
out.push_str(&format!(
" _handle = NativeMethods.{}_{}_new();\n",
prefix.to_lowercase(),
service.name.to_snake_case()
));
out.push_str(" }\n\n");
}
}
for method in &service.configurators {
let method_name = &method.name;
out.push_str(" /// <summary>\n");
out.push_str(&format!(" /// Configure {}.\n", method_name));
out.push_str(" /// </summary>\n");
out.push_str(&format!(" public {class_name} {method_name}("));
for (i, param) in method.params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let ty = csharp_type_for_metadata(¶m.ty, api);
let name = param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", ty, name));
}
out.push_str(") {\n");
out.push_str(" // Store configuration if needed\n");
out.push_str(" return this;\n");
out.push_str(" }\n\n");
}
for reg in &service.registrations {
let reg_method = ®.method;
let service_snake = service.name.to_snake_case();
out.push_str(" /// <summary>\n");
out.push_str(&format!(" /// Register a handler for {}.\n", reg_method));
out.push_str(" /// </summary>\n");
out.push_str(&format!(" public int {}(", reg_method));
for (i, meta_param) in reg.metadata_params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let ty = csharp_type_for_metadata(&meta_param.ty, api);
let name = meta_param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", ty, name));
}
out.push_str(", Delegate handler) {\n");
out.push_str(" var handle = GCHandle.Alloc(handler, GCHandleType.Normal);\n");
out.push_str(" IntPtr ctx = GCHandle.ToIntPtr(handle);\n\n");
out.push_str(&format!(
" int result = NativeMethods.{}_{}_register_{}(\n",
prefix.to_lowercase(),
service_snake,
reg_method.to_snake_case()
));
out.push_str(" _handle,\n");
out.push_str(" _handlerCallback,\n");
out.push_str(" ctx");
for meta_param in ®.metadata_params {
let name = meta_param.name.to_lower_camel_case();
let arg = if matches!(&meta_param.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
{
format!("{}.Handle", name)
} else {
name
};
out.push_str(&format!(",\n {}", arg));
}
out.push_str("\n );\n\n");
out.push_str(" if (result == 0) {\n");
out.push_str(" // Keep the GCHandle alive for the lifetime of the registration\n");
out.push_str(" lock (_registeredCallbacks) {\n");
out.push_str(" _registeredCallbacks[ctx] = handle;\n");
out.push_str(" }\n");
out.push_str(" } else {\n");
out.push_str(" handle.Free();\n");
out.push_str(" }\n");
out.push_str(" return result;\n");
out.push_str(" }\n\n");
for variant in ®.variants {
let variant_method_name = variant.name.to_upper_camel_case();
let variant_fn_name = variant.name.to_snake_case();
out.push_str(" /// <summary>\n");
if let Some(doc) = &variant.doc {
out.push_str(&format!(" /// {}\n", doc));
} else {
out.push_str(&format!(
" /// Register a handler via the {} variant.\n",
variant.name
));
}
out.push_str(" /// </summary>\n");
out.push_str(&format!(" public int {}(", variant_method_name));
for (i, sig_param) in variant.signature_params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let ty = csharp_type_for_metadata(&sig_param.ty, api);
let name = sig_param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", ty, name));
}
out.push_str(", Delegate handler) {\n");
out.push_str(" var handle = GCHandle.Alloc(handler, GCHandleType.Normal);\n");
out.push_str(" IntPtr ctx = GCHandle.ToIntPtr(handle);\n\n");
out.push_str(&format!(
" int result = NativeMethods.{}_{}_{}(\n",
prefix.to_lowercase(),
service_snake,
variant_fn_name
));
out.push_str(" _handle,\n");
out.push_str(" _handlerCallback,\n");
out.push_str(" ctx");
for sig_param in &variant.signature_params {
let name = sig_param.name.to_lower_camel_case();
let arg = if matches!(&sig_param.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
{
format!("{}.Handle", name)
} else {
name
};
out.push_str(&format!(",\n {}", arg));
}
out.push_str("\n );\n\n");
out.push_str(" if (result == 0) {\n");
out.push_str(" lock (_registeredCallbacks) {\n");
out.push_str(" _registeredCallbacks[ctx] = handle;\n");
out.push_str(" }\n");
out.push_str(" } else {\n");
out.push_str(" handle.Free();\n");
out.push_str(" }\n");
out.push_str(" return result;\n");
out.push_str(" }\n\n");
}
}
for ep in &service.entrypoints {
let ep_method = &ep.method;
let service_snake = service.name.to_snake_case();
out.push_str(" /// <summary>\n");
out.push_str(&format!(" /// {}.\n", ep_method));
out.push_str(" /// </summary>\n");
if !entrypoint_return_representable(ep, api) {
continue;
}
let returns_opaque =
matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque));
let return_type = if returns_opaque { "IntPtr" } else { "int" };
out.push_str(&format!(" public {} {}(", return_type, ep_method));
for (i, param) in ep.params.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let ty = csharp_type_for_metadata(¶m.ty, api);
let name = param.name.to_lower_camel_case();
out.push_str(&format!("{} {}", ty, name));
}
out.push_str(") {\n");
out.push_str(&format!(
" return NativeMethods.{}_{}_ep_{}(\n",
prefix.to_lowercase(),
service_snake,
ep_method.to_snake_case()
));
out.push_str(" _handle");
for param in &ep.params {
let name = param.name.to_lower_camel_case();
let arg = if matches!(¶m.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
{
format!("{}.Handle", name)
} else {
name
};
out.push_str(&format!(",\n {}", arg));
}
out.push_str("\n );\n");
out.push_str(" }\n\n");
}
let service_snake = service.name.to_snake_case();
out.push_str(" public void Dispose() {\n");
out.push_str(&format!(
" if (_handle != IntPtr.Zero) {{\n NativeMethods.{}_{}_free(_handle);\n _handle = IntPtr.Zero;\n }}\n",
prefix.to_lowercase(),
service_snake
));
out.push_str(" // Clean up all registered callbacks for this instance\n");
out.push_str(" lock (_registeredCallbacks) {\n");
out.push_str(" var keys = _registeredCallbacks.Keys.ToList();\n");
out.push_str(" foreach (var key in keys) {\n");
out.push_str(" var handle = _registeredCallbacks[key];\n");
out.push_str(" handle.Free();\n");
out.push_str(" _registeredCallbacks.Remove(key);\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" /// <summary>\n");
out.push_str(" /// Unmanaged callback trampoline that recovers the GC handle and invokes the handler.\n");
out.push_str(" /// </summary>\n");
out.push_str(" public static IntPtr HandlerTrampoline(IntPtr ctx, IntPtr requestJson) {\n");
out.push_str(" try {\n");
out.push_str(" // Recover the GCHandle and invoke the delegate\n");
out.push_str(" var handle = GCHandle.FromIntPtr(ctx);\n");
out.push_str(" if (handle.Target is Func<string, string> handler) {\n");
out.push_str(" // Unmarshal the request JSON\n");
out.push_str(" string requestStr = Marshal.PtrToStringUTF8(requestJson) ?? \"{}\";\n");
out.push_str(" // Invoke the handler and get the response\n");
out.push_str(" string responseStr = handler(requestStr);\n");
out.push_str(" // Allocate response string in native memory (malloc-backed on Unix; the\n");
out.push_str(" // native side takes ownership and frees it).\n");
out.push_str(" return Marshal.StringToCoTaskMemUTF8(responseStr);\n");
out.push_str(" }\n");
out.push_str(" } catch {\n");
out.push_str(" // Return null on error\n");
out.push_str(" }\n");
out.push_str(" return IntPtr.Zero;\n");
out.push_str(" }\n\n");
out.push_str("}\n\n"); out.push_str("}\n");
out
}
fn gen_native_methods_cs(api: &ApiSurface, namespace: &str, prefix: &str) -> String {
let mut out = String::new();
out.push_str("// Auto-generated by alef — DO NOT EDIT\n");
out.push_str("#nullable enable\n\n");
out.push_str(&format!("namespace {namespace} {{\n\n"));
out.push_str("using System;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
out.push_str("/// <summary>\n");
out.push_str("/// Native P/Invoke declarations for service API.\n");
out.push_str("/// </summary>\n");
out.push_str("internal static partial class NativeMethods {\n\n");
out.push_str(" /// <summary>\n");
out.push_str(" /// Native callback signature for service handlers.\n");
out.push_str(" /// </summary>\n");
out.push_str(" [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n");
out.push_str(" public delegate IntPtr HandlerCallback(IntPtr ctx, IntPtr requestJson);\n\n");
for service in &api.services {
let service_snake = service.name.to_snake_case();
out.push_str(&format!(
" [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
prefix.to_lowercase()
));
out.push_str(&format!(
" public static extern IntPtr {}_{}_new();\n\n",
prefix.to_lowercase(),
service_snake
));
out.push_str(&format!(
" [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
prefix.to_lowercase()
));
out.push_str(&format!(
" public static extern void {}_{}_free(IntPtr ptr);\n\n",
prefix.to_lowercase(),
service_snake
));
for reg in &service.registrations {
let reg_method_snake = reg.method.to_snake_case();
out.push_str(&format!(
" [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
prefix.to_lowercase()
));
out.push_str(&format!(
" public static extern int {}_{}_register_{}(\n",
prefix.to_lowercase(),
service_snake,
reg_method_snake
));
out.push_str(" IntPtr owner,\n");
out.push_str(" HandlerCallback callback,\n");
out.push_str(" IntPtr ctx");
for meta_param in ®.metadata_params {
let c_type = match &meta_param.ty {
TypeRef::String | TypeRef::Char => "string",
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "int",
PrimitiveType::U8 => "byte",
PrimitiveType::U16 => "ushort",
PrimitiveType::U32 => "uint",
PrimitiveType::U64 => "ulong",
PrimitiveType::I8 => "sbyte",
PrimitiveType::I16 => "short",
PrimitiveType::I32 => "int",
PrimitiveType::I64 => "long",
PrimitiveType::F32 => "float",
PrimitiveType::F64 => "double",
PrimitiveType::Usize => "nuint",
PrimitiveType::Isize => "nint",
}
}
_ => "IntPtr",
};
out.push_str(&format!(",\n {} {}", c_type, meta_param.name));
}
out.push_str("\n );\n\n");
for variant in ®.variants {
let variant_fn_name = variant.name.to_snake_case();
out.push_str(&format!(
" [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
prefix.to_lowercase()
));
out.push_str(&format!(
" public static extern int {}_{}_{}(\n",
prefix.to_lowercase(),
service_snake,
variant_fn_name
));
out.push_str(" IntPtr owner,\n");
out.push_str(" HandlerCallback callback,\n");
out.push_str(" IntPtr ctx");
for sig_param in &variant.signature_params {
let c_type = match &sig_param.ty {
TypeRef::String | TypeRef::Char => "string",
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "int",
PrimitiveType::U8 => "byte",
PrimitiveType::U16 => "ushort",
PrimitiveType::U32 => "uint",
PrimitiveType::U64 => "ulong",
PrimitiveType::I8 => "sbyte",
PrimitiveType::I16 => "short",
PrimitiveType::I32 => "int",
PrimitiveType::I64 => "long",
PrimitiveType::F32 => "float",
PrimitiveType::F64 => "double",
PrimitiveType::Usize => "nuint",
PrimitiveType::Isize => "nint",
}
}
_ => "IntPtr",
};
out.push_str(&format!(",\n {} {}", c_type, sig_param.name));
}
out.push_str("\n );\n\n");
}
}
for ep in &service.entrypoints {
if !entrypoint_return_representable(ep, api) {
continue;
}
let ep_method_snake = ep.method.to_snake_case();
let returns_opaque =
matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque));
let return_type = if returns_opaque { "IntPtr" } else { "int" };
out.push_str(&format!(
" [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
prefix.to_lowercase()
));
out.push_str(&format!(
" public static extern {} {}_{}_ep_{}(\n",
return_type,
prefix.to_lowercase(),
service_snake,
ep_method_snake
));
out.push_str(" IntPtr owner");
for param in &ep.params {
let c_type = match ¶m.ty {
TypeRef::String | TypeRef::Char => "string",
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "int",
PrimitiveType::U8 => "byte",
PrimitiveType::U16 => "ushort",
PrimitiveType::U32 => "uint",
PrimitiveType::U64 => "ulong",
PrimitiveType::I8 => "sbyte",
PrimitiveType::I16 => "short",
PrimitiveType::I32 => "int",
PrimitiveType::I64 => "long",
PrimitiveType::F32 => "float",
PrimitiveType::F64 => "double",
PrimitiveType::Usize => "nuint",
PrimitiveType::Isize => "nint",
}
}
_ => "IntPtr",
};
out.push_str(&format!(",\n {} {}", c_type, param.name));
}
out.push_str("\n );\n\n");
}
}
out.push_str("}\n\n"); out.push_str("}\n");
out
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let namespace = config.csharp_namespace();
let prefix = config.ffi_prefix();
let output_dir = config
.output_paths
.get("csharp")
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| "packages/csharp/".to_owned());
let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));
let mut files = Vec::new();
for service in &api.services {
let service_cs = gen_service_cs(api, service, &namespace, &prefix);
let class_name = to_csharp_name(&service.name);
files.push(GeneratedFile {
path: base_path.join(format!("{}.cs", class_name)),
content: service_cs,
generated_header: false, });
}
let native_methods = gen_native_methods_cs(api, &namespace, &prefix);
files.push(GeneratedFile {
path: base_path.join("ServiceNativeMethods.cs"),
content: native_methods,
generated_header: false,
});
Ok(files)
}
#[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![
crate::core::ir::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(),
},
crate::core::ir::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: None,
style: Default::default(),
},
],
};
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_cs_contains_class() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(cs.contains("public class TestService"));
assert!(cs.contains("private IntPtr _handle"));
assert!(cs.contains("public TestService()"));
}
#[test]
fn test_gen_service_cs_contains_registration_method() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(cs.contains("public int add_handler("));
assert!(cs.contains("GCHandle.Alloc(handler, GCHandleType.Normal)"));
assert!(cs.contains("_handlerCallback"));
assert!(cs.contains("_registeredCallbacks[ctx] = handle"));
}
#[test]
fn test_gen_service_cs_contains_run_method() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(cs.contains("public int run("));
assert!(cs.contains("NativeMethods.test_test_service_ep_run"));
}
#[test]
fn test_gen_service_cs_contains_unmanaged_callback() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(cs.contains("public static IntPtr HandlerTrampoline"));
assert!(cs.contains("_handlerCallback = HandlerTrampoline"));
assert!(cs.contains("GCHandle.FromIntPtr(ctx)"));
assert!(cs.contains("Marshal.PtrToStringUTF8"));
}
#[test]
fn test_gen_service_cs_trampoline_invokes_delegate() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(
cs.contains("if (handle.Target is Func<string, string> handler)"),
"trampoline must cast to Func<string, string>"
);
assert!(
cs.contains("handler(requestStr)"),
"trampoline must invoke the handler with request string"
);
assert!(
cs.contains("string responseStr = handler(requestStr);"),
"trampoline must capture delegate result into responseStr"
);
assert!(
!cs.contains("\"stub implementation\""),
"trampoline must not have stub implementation comment"
);
assert!(
!cs.contains("string responseStr = \"{}\""),
"trampoline must not return hardcoded {{}} response"
);
assert!(
cs.contains("Marshal.StringToCoTaskMemUTF8(responseStr)"),
"trampoline must marshal the response back to native memory"
);
}
#[test]
fn test_gen_native_methods_cs_contains_callback_typedef() {
let api = make_fixture_surface();
let native = gen_native_methods_cs(&api, "MyNamespace", "test");
assert!(native.contains("delegate IntPtr HandlerCallback"));
assert!(native.contains("[UnmanagedFunctionPointer(CallingConvention.Cdecl)]"));
}
#[test]
fn test_gen_native_methods_cs_contains_pinvoke_decls() {
let api = make_fixture_surface();
let native = gen_native_methods_cs(&api, "MyNamespace", "test");
assert!(native.contains("[DllImport("));
assert!(native.contains("test_test_service_new()"));
assert!(native.contains("test_test_service_free"));
assert!(native.contains("test_test_service_register_add_handler"));
assert!(native.contains("test_test_service_ep_run"));
}
#[test]
fn test_generate_returns_files() {
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!(!files.is_empty(), "expected at least one file");
let has_service_class = files
.iter()
.any(|f| f.path.to_string_lossy().contains("TestService.cs"));
let has_native_methods = files
.iter()
.any(|f| f.path.to_string_lossy().contains("ServiceNativeMethods.cs"));
assert!(has_service_class, "expected TestService.cs in output");
assert!(has_native_methods, "expected ServiceNativeMethods.cs in output");
}
#[test]
fn test_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");
}
#[test]
fn test_gen_service_cs_contains_variant_methods() {
let api = make_fixture_surface();
let service = &api.services[0];
let cs = gen_service_cs(&api, service, "MyNamespace", "test");
assert!(
cs.contains("public int Get("),
"expected Get variant method in service class"
);
assert!(
cs.contains("Register a GET handler"),
"expected Get variant documentation"
);
assert!(
cs.contains("public int Post("),
"expected Post variant method in service class"
);
assert!(
cs.contains("Register a handler via the post variant"),
"expected Post variant auto-generated documentation"
);
assert!(
cs.contains("NativeMethods.test_test_service_get("),
"expected Get variant P/Invoke call"
);
assert!(
cs.contains("NativeMethods.test_test_service_post("),
"expected Post variant P/Invoke call"
);
}
#[test]
fn test_gen_native_methods_cs_contains_variant_pinvoke_decls() {
let api = make_fixture_surface();
let native = gen_native_methods_cs(&api, "MyNamespace", "test");
assert!(
native.contains("public static extern int test_test_service_get("),
"expected Get variant P/Invoke declaration"
);
assert!(
native.contains("public static extern int test_test_service_post("),
"expected Post variant P/Invoke declaration"
);
assert!(
native.contains("IntPtr owner,") && native.contains("HandlerCallback callback,"),
"expected variant P/Invoke to have owner and callback parameters"
);
}
}