use std::path::PathBuf;
use super::CALL_ARENA_BUF_LEN;
use super::CodeGenerator;
use super::GeneratedFile;
use super::GeneratedFiles;
use super::collect_peer_contracts;
use super::peer_min_version;
use crate::ir::AbiBuiltin;
use crate::ir::EnumDef;
use crate::ir::PrimitiveType;
use crate::ir::ResolvedBundle;
use crate::ir::ResolvedContract;
use crate::ir::ResolvedFunction;
use crate::ir::ResolvedHostContract;
use crate::ir::ResolvedParam;
use crate::ir::ResolvedType;
use crate::ir::ResolvedTypeRef;
use crate::ir::ValidatedIr;
use polyplug_codegen::PolyplugcError;
pub(crate) struct CSharpGenerator;
const CS_HEADER: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --api api.toml --lang csharp --out <dir>\n\n";
fn cs_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "byte".to_owned(), },
ResolvedTypeRef::AbiType(abi) => match abi {
AbiBuiltin::StringView => "Polyplug.Abi.StringView".to_owned(),
AbiBuiltin::Buffer => "Polyplug.Abi.Buffer".to_owned(),
AbiBuiltin::Ptr => "IntPtr".to_owned(),
AbiBuiltin::Void => "void".to_owned(),
},
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn generate_cs_user_type(ty: &ResolvedType) -> String {
let mut out: String = String::new();
out.push_str("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]\n");
out.push_str(&format!("public struct {} {{\n", ty.name));
for field in &ty.fields {
let cs_ty: String = cs_type_name(&field.ty);
out.push_str(&format!(" public {} {};\n", cs_ty, field.name));
}
out.push_str("}\n");
out
}
fn generate_cs_enum(e: &EnumDef) -> String {
let repr_cs: &str = e.repr.cs_name();
let mut out: String = String::new();
if e.bitflag {
out.push_str("[Flags]\n");
}
out.push_str(&format!("public enum {} : {} {{\n", e.name, repr_cs));
for variant in &e.variants {
out.push_str(&format!(" {} = {},\n", variant.name, variant.value));
}
out.push_str("}\n");
out
}
fn needs_arg_pack(params: &[crate::ir::ResolvedParam]) -> bool {
params.len() >= 2
}
fn fn_needs_arena(func: &ResolvedFunction) -> bool {
matches!(
&func.returns,
Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView))
| Some(ResolvedTypeRef::AbiType(AbiBuiltin::Buffer))
| Some(ResolvedTypeRef::UserDefined(_))
)
}
fn contract_needs_arena(contract: &ResolvedContract) -> bool {
contract.functions.iter().any(fn_needs_arena)
}
fn cs_assembly_namespace(file: &polyplug_codegen::ResolvedBundleFile) -> Option<String> {
let raw_path: &str = match file {
polyplug_codegen::ResolvedBundleFile::Single(path) => path.as_str(),
polyplug_codegen::ResolvedBundleFile::PlatformMap(map) => {
map.values().next().map(String::as_str)?
}
};
let file_name: &str = raw_path.rsplit(['/', '\\']).next().unwrap_or(raw_path);
let stem: &str = file_name
.rsplit_once('.')
.map(|(s, _)| s)
.unwrap_or(file_name);
if stem.is_empty() {
return None;
}
Some(stem.to_owned())
}
fn emit_cs_call_arena_helpers(out: &mut String) {
out.push_str(
"/// <summary>Inline call-arena helpers (port of polyplug_abi::CallArena).</summary>\n",
);
out.push_str("internal static unsafe class CallArenaOps {\n");
out.push_str(" /// <summary>Size of each caller's inline call-arena buffer.</summary>\n");
out.push_str(" ///\n");
out.push_str(
" /// Variable-size VM return values (strings, buffers) are bump-allocated from\n",
);
out.push_str(
" /// this buffer; outputs larger than it spill into host-allocated overflow\n",
);
out.push_str(" /// blocks that are retained across resets and freed only by FreeAll.\n");
out.push_str(&format!(
" public const nuint CALL_ARENA_BUF_LEN = {CALL_ARENA_BUF_LEN};\n"
));
out.push_str(
" /// <summary>Alignment used to free host-allocated overflow blocks.</summary>\n",
);
out.push_str(" public static readonly nuint OVERFLOW_BLOCK_ALIGN = (nuint)IntPtr.Size;\n\n");
out.push_str(
" /// <summary>Construct a CallArena over `buf` (primary region) with `host` for overflow.</summary>\n",
);
out.push_str(" public static CallArena New(byte* buf, nuint len, IntPtr host) {\n");
out.push_str(" return new CallArena {\n");
out.push_str(" Cur = (IntPtr)buf,\n");
out.push_str(" End = (IntPtr)(buf + len),\n");
out.push_str(" Base = (IntPtr)buf,\n");
out.push_str(" Host = host,\n");
out.push_str(" FirstOverflow = IntPtr.Zero,\n");
out.push_str(" };\n");
out.push_str(" }\n\n");
out.push_str(
" /// <summary>Rewind `arena` for reuse: the primary region and every retained\n",
);
out.push_str(
" /// overflow block become available again. Overflow blocks are NOT freed —\n",
);
out.push_str(
" /// they are retained for the next call. Call FreeAll before releasing the\n",
);
out.push_str(
" /// arena. After reset, all pointers previously returned by arena allocations\n",
);
out.push_str(" /// are invalid.</summary>\n");
out.push_str(" public static void Reset(CallArena* arena) {\n");
out.push_str(" arena->Cur = arena->Base;\n");
out.push_str(" IntPtr block = arena->FirstOverflow;\n");
out.push_str(" while (block != IntPtr.Zero) {\n");
out.push_str(" var hdr = (ArenaOverflowBlock*)block;\n");
out.push_str(" hdr->Used = (nuint)sizeof(ArenaOverflowBlock);\n");
out.push_str(" block = hdr->Next;\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" /// <summary>Free all retained overflow blocks and clear the chain.\n");
out.push_str(
" /// Call this before releasing the arena's backing buffer (e.g. in Dispose).</summary>\n",
);
out.push_str(" public static void FreeAll(CallArena* arena) {\n");
out.push_str(" IntPtr block = arena->FirstOverflow;\n");
out.push_str(" while (block != IntPtr.Zero) {\n");
out.push_str(" var hdr = (ArenaOverflowBlock*)block;\n");
out.push_str(" IntPtr next = hdr->Next;\n");
out.push_str(" nuint capacity = hdr->Capacity;\n");
out.push_str(" if (arena->Host != IntPtr.Zero) {\n");
out.push_str(" var hostApi = (HostApi*)arena->Host;\n");
out.push_str(" var freeFn = (delegate* unmanaged[Cdecl]<IntPtr, IntPtr, nuint, nuint, void>)hostApi->Free;\n");
out.push_str(" freeFn(arena->Host, block, capacity, OVERFLOW_BLOCK_ALIGN);\n");
out.push_str(" }\n");
out.push_str(" block = next;\n");
out.push_str(" }\n");
out.push_str(" arena->FirstOverflow = IntPtr.Zero;\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn emit_cs_arg_pack(contract_struct: &str, func: &crate::ir::ResolvedFunction) -> String {
let mut out: String = String::new();
out.push_str("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]\n");
out.push_str(&format!(
"public struct {}{}Args {{\n",
contract_struct,
pascal_case(&func.name)
));
for param in &func.params {
let cs_ty: String = cs_type_name(¶m.ty);
out.push_str(&format!(
" public {} {};\n",
cs_ty,
pascal_case(¶m.name)
));
}
out.push_str("}\n");
out
}
fn pascal_case(s: &str) -> String {
s.split(['_', '.'])
.filter(|seg: &&str| !seg.is_empty())
.map(|seg: &str| {
let mut c: core::str::Chars<'_> = seg.chars();
match c.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<String>>()
.join("")
}
fn contract_name_to_cs_interface(name: &str) -> String {
format!("I{}GuestContract", pascal_case(name))
}
fn contract_name_to_cs_class(name: &str) -> String {
format!("{}Contract", pascal_case(name))
}
fn contract_name_to_upper_snake(name: &str) -> String {
name.replace('.', "_").to_uppercase()
}
fn cs_return_type(func: &ResolvedFunction) -> String {
match &func.returns {
Some(ty) => cs_type_name(ty),
None => "void".to_owned(),
}
}
fn generate_cs_types_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
for e in &ir.enums {
out.push_str(&generate_cs_enum(e));
out.push('\n');
}
for ty in &ir.types {
out.push_str(&generate_cs_user_type(ty));
out.push('\n');
}
for contract in &ir.contracts {
let struct_name: String = pascal_case(&contract.name) + "Contract";
for func in &contract.functions {
if needs_arg_pack(&func.params) {
out.push_str(&emit_cs_arg_pack(&struct_name, func));
out.push('\n');
}
}
}
out
}
fn generate_cs_host_types_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Host;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
out.push_str("namespace Polyplug.Generated;\n\n");
out.push_str("public static class ContractIds {\n");
for contract in &ir.contracts {
let contract_upper: String = contract.name.to_uppercase().replace(['.', '-'], "_");
out.push_str(&format!(
" public const ulong {}_CONTRACT_ID = 0x{:016X};\n",
contract_upper, contract.contract_id
));
}
out.push_str("}\n\n");
for e in &ir.enums {
out.push_str(&generate_cs_enum(e));
out.push('\n');
}
for ty in &ir.types {
out.push_str(&generate_cs_user_type(ty));
out.push('\n');
}
out
}
fn generate_cs_guest_contracts(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n\n");
for contract in &ir.contracts {
let iface_name: String = contract_name_to_cs_interface(&contract.name);
out.push_str(&format!(
"/// Guest interface for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str(&format!("public interface {} {{\n", iface_name));
for func in &contract.functions {
let ret: String = cs_return_type(func);
let method_name: String = pascal_case(&func.name);
if func.params.is_empty() {
out.push_str(&format!(" {} {}();\n", ret, method_name));
} else if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let is_struct: bool = matches!(¶m.ty, ResolvedTypeRef::UserDefined(_));
if is_struct {
let cs_ty: String = cs_type_name(¶m.ty);
out.push_str(&format!(
" {} {}(ref {} {});\n",
ret, method_name, cs_ty, param.name
));
} else {
let cs_ty: String = cs_type_name(¶m.ty);
out.push_str(&format!(
" {} {}({} {});\n",
ret, method_name, cs_ty, param.name
));
}
} else {
let params_str: String = func
.params
.iter()
.map(|p: &ResolvedParam| format!("{} {}", cs_type_name(&p.ty), p.name))
.collect::<Vec<String>>()
.join(", ");
out.push_str(&format!(" {} {}({});\n", ret, method_name, params_str));
}
}
out.push_str("}\n\n");
}
out
}
fn generate_cs_guest_interfaces(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
if let Some(bundle) = &ir.bundle {
for plugin in &bundle.plugins {
for contract_impl in &plugin.implements {
if let Some(contract) = ir.contracts.iter().find(|c| {
let contract_full: String =
format!("{}@{}.{}", c.name, c.version.major, c.version.minor);
&contract_full == contract_impl
}) {
generate_cs_guest_plugin_interface(&mut out, &plugin.name, contract);
}
}
}
} else {
for contract in &ir.contracts {
let lower: String = contract.name.replace('.', "_");
let class_name: String = contract_name_to_cs_class(&contract.name);
let upper: String = contract_name_to_upper_snake(&contract.name);
let iface_name: String = contract_name_to_cs_interface(&contract.name);
let contract_id: u64 = contract.contract_id;
let fn_count: usize = contract.functions.len();
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(
"public static class {}Interfaces {{\n",
class_name
));
out.push_str(&format!(
" public const ulong {upper}_CONTRACT_ID = 0x{contract_id:016X}UL;\n"
));
emit_cs_guest_instance_machinery(&mut out, &upper, &lower, &class_name, &iface_name);
let state_class: String = format!("{class_name}InstanceState");
for func in &contract.functions {
let fn_name: String = func.name.replace('-', "_");
let abi_method: String = format!("{lower}_{fn_name}_abi");
out.push_str(
" [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n",
);
out.push_str(&format!(
" private static unsafe void {}(GuestContractInstance instance, IntPtr argsPtr, IntPtr outPtr, AbiError* outErr) {{\n",
abi_method
));
emit_cs_guest_dispatch_body(&mut out, &state_class, &class_name, func);
out.push_str(" }\n\n");
}
out.push_str(&format!(
" private static readonly IntPtr[] {upper}_FNS;\n"
));
out.push_str(&format!(
" private static System.Runtime.InteropServices.GCHandle _{upper}_pin_handle;\n"
));
out.push_str(&format!(
" public static GuestContractInterface {upper}_INTERFACE;\n\n"
));
out.push_str(&format!(" static {class_name}Interfaces() {{\n"));
out.push_str(" unsafe {\n");
out.push_str(&format!(" {upper}_FNS = new IntPtr[] {{\n"));
for func in &contract.functions {
let fn_name: String = func.name.replace('-', "_");
let abi_method: String = format!("{lower}_{fn_name}_abi");
out.push_str(&format!(
" (IntPtr)(delegate* unmanaged[Cdecl]<GuestContractInstance, IntPtr, IntPtr, AbiError*, void>)&{abi_method},\n"
));
}
out.push_str(" };\n");
out.push_str(&format!(
" _{upper}_pin_handle = System.Runtime.InteropServices.GCHandle.Alloc({upper}_FNS, System.Runtime.InteropServices.GCHandleType.Pinned);\n"
));
out.push_str(&format!(
" {upper}_INTERFACE = new GuestContractInterface {{\n"
));
out.push_str(&format!(
" ContractId = {upper}_CONTRACT_ID,\n"
));
out.push_str(&format!(
" ContractVersion = new Polyplug.Abi.Version {{ Major = {major}u, Minor = {minor}u, Patch = {patch}u }},\n"
));
out.push_str(" DispatchType = DispatchType.Native,\n");
out.push_str(&format!(
" CreateInstance = (IntPtr)(delegate* unmanaged[Cdecl]<VmLoaderData, IntPtr, IntPtr, GuestContractInstance*, void>)&{upper}_CreateInstance,\n"
));
out.push_str(&format!(
" DestroyInstance = (IntPtr)(delegate* unmanaged[Cdecl]<VmLoaderData, IntPtr, GuestContractInstance, void>)&{upper}_DestroyInstance,\n"
));
out.push_str(" Dispatch = new DispatchMechanisms {\n");
out.push_str(" Native = new NativeDispatch {\n");
out.push_str(&format!(
" FunctionCount = {fn_count}u,\n"
));
out.push_str(&format!(
" Functions = _{upper}_pin_handle.AddrOfPinnedObject(),\n"
));
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
}
out
}
fn generate_cs_guest_plugin_interface(
out: &mut String,
plugin_name: &str,
contract: &ResolvedContract,
) {
let plugin_upper: String = plugin_name.to_uppercase().replace('.', "_");
let plugin_lower: String = plugin_name.to_lowercase().replace('.', "_");
let iface_name: String = contract_name_to_cs_interface(&contract.name);
let contract_id: u64 = contract.contract_id;
let fn_count: usize = contract.functions.len();
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
let class_name_pascal: String = plugin_name
.split('_')
.map(|s| {
let mut chars: core::str::Chars<'_> = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join("");
out.push_str(&format!("// Plugin: {}\n", plugin_name));
out.push_str(&format!(
"public static class {}Interfaces {{\n",
class_name_pascal
));
out.push_str(&format!(
" public const ulong {upper}_CONTRACT_ID = 0x{contract_id:016X}UL;\n",
upper = plugin_upper
));
emit_cs_guest_instance_machinery(
out,
&plugin_upper,
&plugin_lower,
&class_name_pascal,
&iface_name,
);
let state_class: String = format!("{class_name_pascal}InstanceState");
let arg_pack_prefix: String = pascal_case(&contract.name) + "Contract";
for func in &contract.functions {
let fn_name: String = func.name.replace('-', "_");
let abi_method: String = format!("{lower}_{fn_name}_abi", lower = plugin_lower);
out.push_str(" [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
" private static unsafe void {}(GuestContractInstance instance, IntPtr argsPtr, IntPtr outPtr, AbiError* outErr) {{\n",
abi_method
));
emit_cs_guest_dispatch_body(out, &state_class, &arg_pack_prefix, func);
out.push_str(" }\n\n");
}
out.push_str(&format!(
" private static readonly IntPtr[] {upper}_FNS;\n",
upper = plugin_upper
));
out.push_str(&format!(
" private static System.Runtime.InteropServices.GCHandle _{upper}_pin_handle;\n",
upper = plugin_upper
));
out.push_str(&format!(
" public static GuestContractInterface {upper}_INTERFACE;\n\n",
upper = plugin_upper
));
out.push_str(&format!(
" static {class_name}Interfaces() {{\n",
class_name = class_name_pascal
));
out.push_str(" unsafe {\n");
out.push_str(&format!(
" {upper}_FNS = new IntPtr[] {{\n",
upper = plugin_upper
));
for func in &contract.functions {
let fn_name: String = func.name.replace('-', "_");
let abi_method: String = format!("{lower}_{fn_name}_abi", lower = plugin_lower);
out.push_str(&format!(
" (IntPtr)(delegate* unmanaged[Cdecl]<GuestContractInstance, IntPtr, IntPtr, AbiError*, void>)&{abi_method},\n"
));
}
out.push_str(" };\n");
out.push_str(&format!(
" _{upper}_pin_handle = System.Runtime.InteropServices.GCHandle.Alloc({upper}_FNS, System.Runtime.InteropServices.GCHandleType.Pinned);\n",
upper = plugin_upper
));
out.push_str(&format!(
" {upper}_INTERFACE = new GuestContractInterface {{\n",
upper = plugin_upper
));
out.push_str(&format!(
" ContractId = {upper}_CONTRACT_ID,\n",
upper = plugin_upper
));
out.push_str(&format!(
" ContractVersion = new Polyplug.Abi.Version {{ Major = {major}u, Minor = {minor}u, Patch = {patch}u }},\n"
));
out.push_str(" DispatchType = DispatchType.Native,\n");
out.push_str(&format!(
" CreateInstance = (IntPtr)(delegate* unmanaged[Cdecl]<VmLoaderData, IntPtr, IntPtr, GuestContractInstance*, void>)&{upper}_CreateInstance,\n",
upper = plugin_upper
));
out.push_str(&format!(
" DestroyInstance = (IntPtr)(delegate* unmanaged[Cdecl]<VmLoaderData, IntPtr, GuestContractInstance, void>)&{upper}_DestroyInstance,\n",
upper = plugin_upper
));
out.push_str(" Dispatch = new DispatchMechanisms {\n");
out.push_str(" Native = new NativeDispatch {\n");
out.push_str(&format!(
" FunctionCount = {fn_count}u,\n"
));
out.push_str(&format!(
" Functions = _{upper}_pin_handle.AddrOfPinnedObject(),\n",
upper = plugin_upper
));
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn emit_cs_guest_instance_machinery(
out: &mut String,
upper: &str,
lower: &str,
class_pascal: &str,
iface_name: &str,
) {
out.push_str(&format!(
" private static Func<IntPtr, {iface_name}>? _factory_{lower};\n"
));
out.push_str(" /// <summary>\n");
out.push_str(" /// Register the author factory; call once at module initialization\n");
out.push_str(
" /// (<c>[ModuleInitializer]</c>). The factory receives the HostApi pointer\n",
);
out.push_str(" /// at instance creation, so every implementation is constructed with its\n");
out.push_str(" /// owning runtime's host — no host pointer is stored in the SDK.\n");
out.push_str(" /// </summary>\n");
out.push_str(&format!(
" public static void Set{class_pascal}Factory(Func<IntPtr, {iface_name}> factory) {{ _factory_{lower} = factory; }}\n\n"
));
out.push_str(" /// <summary>Per-instance payload carried in GuestContractInstance.Data (via GCHandle).</summary>\n");
out.push_str(&format!(
" private sealed class {class_pascal}InstanceState {{\n"
));
out.push_str(
" // Host pointer captured at instance creation — routes every host call\n",
);
out.push_str(" // (allocation, logging, peer dispatch) to the runtime that owns it.\n");
out.push_str(" public IntPtr Host;\n");
out.push_str(&format!(
" // The author's implementation, created by the Set{class_pascal}Factory factory.\n"
));
out.push_str(&format!(" public {iface_name} Impl = null!;\n"));
out.push_str(" }\n\n");
out.push_str(" [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
" private static unsafe void {upper}_CreateInstance(VmLoaderData loaderData, IntPtr host, IntPtr args, GuestContractInstance* outInstance) {{\n"
));
out.push_str(
" _ = loaderData; // Native-dispatch contracts ignore the VM loader handle.\n",
);
out.push_str(
" // Calls the author factory and carries the payload in instance.Data as a\n",
);
out.push_str(" // normal (non-pinned) GCHandle — an opaque token the host never\n");
out.push_str(" // dereferences. Writes a null handle when host is null, the factory\n");
out.push_str(" // was not registered, or it throws.\n");
out.push_str(" if (outInstance == null) return;\n");
out.push_str(" try {\n");
out.push_str(&format!(" var factory = _factory_{lower};\n"));
out.push_str(" if (host == IntPtr.Zero || factory is null) {\n");
out.push_str(
" *outInstance = new GuestContractInstance { Data = IntPtr.Zero };\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(&format!(
" var state = new {class_pascal}InstanceState {{ Host = host, Impl = factory(host) }};\n"
));
out.push_str(
" var handle = System.Runtime.InteropServices.GCHandle.Alloc(state);\n",
);
out.push_str(" *outInstance = new GuestContractInstance {\n");
out.push_str(
" Data = System.Runtime.InteropServices.GCHandle.ToIntPtr(handle),\n",
);
out.push_str(&format!(
" ContractId = {upper}_CONTRACT_ID,\n"
));
out.push_str(" };\n");
out.push_str(" } catch {\n");
out.push_str(" *outInstance = new GuestContractInstance { Data = IntPtr.Zero };\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
" private static void {upper}_DestroyInstance(VmLoaderData loaderData, IntPtr host, GuestContractInstance instance) {{\n"
));
out.push_str(
" _ = loaderData; // Native-dispatch contracts ignore the VM loader handle.\n",
);
out.push_str(
" // Frees the GCHandle allocated by CreateInstance; the payload becomes\n",
);
out.push_str(" // collectible. The host calls destroy exactly once per instance.\n");
out.push_str(" if (instance.Data == IntPtr.Zero) return;\n");
out.push_str(" try {\n");
out.push_str(
" System.Runtime.InteropServices.GCHandle.FromIntPtr(instance.Data).Free();\n",
);
out.push_str(" } catch {\n");
out.push_str(
" // Foreign or double-freed handle: nothing safe to do in a native callback.\n",
);
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn emit_cs_guest_dispatch_body(
out: &mut String,
state_class: &str,
contract_struct: &str,
func: &ResolvedFunction,
) {
let method_name: String = pascal_case(&func.name);
let has_return: bool = func.returns.is_some();
let has_params: bool = !func.params.is_empty();
out.push_str(" if (outErr == null) return;\n");
out.push_str(" try {\n");
out.push_str(" if (instance.Data == IntPtr.Zero) {\n");
out.push_str(
" *outErr = new AbiError { Code = (uint)AbiErrorCode.InvalidPointer, Message = StringViewHelper.StaticMessage(\"instance is null\") };\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
if has_params {
out.push_str(" if (argsPtr == IntPtr.Zero) {\n");
out.push_str(
" *outErr = new AbiError { Code = (uint)AbiErrorCode.InvalidPointer };\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
}
if has_return {
out.push_str(" if (outPtr == IntPtr.Zero) {\n");
out.push_str(
" *outErr = new AbiError { Code = (uint)AbiErrorCode.InvalidPointer };\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
}
out.push_str(
" // instance.Data is the GCHandle token produced by CreateInstance.\n",
);
out.push_str(&format!(
" var state = ({state_class}?)System.Runtime.InteropServices.GCHandle.FromIntPtr(instance.Data).Target;\n"
));
out.push_str(" if (state is null) {\n");
out.push_str(" *outErr = new AbiError { Code = (uint)AbiErrorCode.InvalidPointer, Message = StringViewHelper.StaticMessage(\"instance payload collected\") };\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" var impl = state.Impl;\n");
let call_args: String = if !has_params {
String::new()
} else if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let cs_ty: String = cs_type_name(¶m.ty);
match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" ref var {0} = ref *({1}*)argsPtr;\n",
param.name, cs_ty
));
format!("ref {}", param.name)
}
_ => {
out.push_str(&format!(
" var {0} = *({1}*)argsPtr;\n",
param.name, cs_ty
));
param.name.clone()
}
}
} else {
let struct_name: String = format!("{}{}Args", contract_struct, method_name);
out.push_str(&format!(
" var packed = *({struct_name}*)argsPtr;\n"
));
func.params
.iter()
.map(|p: &ResolvedParam| format!("packed.{}", pascal_case(&p.name)))
.collect::<Vec<String>>()
.join(", ")
};
if has_return {
let ret_ty: String = cs_return_type(func);
out.push_str(&format!(
" var result = impl.{method_name}({call_args});\n"
));
out.push_str(&format!(" *({ret_ty}*)outPtr = result;\n"));
} else {
out.push_str(&format!(" impl.{method_name}({call_args});\n"));
}
out.push_str(" *outErr = new AbiError { Code = (uint)AbiErrorCode.Ok };\n");
out.push_str(" } catch (Polyplug.Guest.GuestException ex) {\n");
out.push_str(" var msg = StringViewHelper.StaticMessage(ex.Message);\n");
out.push_str(" *outErr = new AbiError { Code = ex.Code, Message = msg };\n");
out.push_str(" } catch {\n");
out.push_str(" var msg = StringViewHelper.StaticMessage(\"plugin panicked\");\n");
out.push_str(
" *outErr = new AbiError { Code = (uint)AbiErrorCode.Panic, Message = msg };\n",
);
out.push_str(" }\n");
}
fn generate_cs_guest_init(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using System.Runtime.InteropServices;\n");
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n\n");
let assembly_namespace: Option<String> = ir
.bundle
.as_ref()
.and_then(|b: &crate::ir::ResolvedBundle| cs_assembly_namespace(&b.file));
if let Some(ns) = &assembly_namespace {
out.push_str(&format!("namespace {ns};\n\n"));
}
out.push_str("public static class Plugin {\n");
out.push_str(" [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }, EntryPoint = \"polyplug_init\")]\n");
out.push_str(" public static AbiError PolyplugInit(IntPtr hostPtr, IntPtr ctxPtr) {\n");
out.push_str(" if (hostPtr == IntPtr.Zero || ctxPtr == IntPtr.Zero)\n");
out.push_str(" return new AbiError { Code = (uint)AbiErrorCode.Generic, Message = StringViewHelper.StaticMessage(\"null host or ctx pointer in polyplug_init\") };\n");
out.push_str(" // No process-wide host storage: the host pointer reaches each\n");
out.push_str(" // implementation through CreateInstance -> author factory.\n");
out.push_str(" System.Threading.Thread.BeginThreadAffinity();\n");
out.push_str(" try {\n");
out.push_str(" unsafe {\n");
if let Some(bundle) = &ir.bundle {
for plugin in &bundle.plugins {
for contract_impl in &plugin.implements {
if let Some(contract) = ir.contracts.iter().find(|c| {
let contract_full: String =
format!("{}@{}.{}", c.name, c.version.major, c.version.minor);
&contract_full == contract_impl
}) {
let plugin_upper: String = plugin.name.to_uppercase().replace(['.', '-'], "_");
let plugin_lower: String = plugin.name.to_lowercase().replace(['.', '-'], "_");
let plugin_pascal: String = plugin
.name
.split(['_', '.', '-'])
.map(|s| {
let mut chars: core::str::Chars<'_> = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join("");
let contract_name_full: String =
format!("{}@{}", contract.name, contract.version.major);
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(
" // Register {} ({})\n",
plugin.name, contract_name_full
));
out.push_str(&format!(
" var plugin_name_{plugin_lower} = System.Text.Encoding.UTF8.GetBytes(\"{plugin_lower}\");\n"
));
out.push_str(&format!(
" var contract_name_{plugin_lower} = System.Text.Encoding.UTF8.GetBytes(\"{}\");\n",
contract_name_full
));
out.push_str(&format!(
" var nameHandle_{plugin_lower} = System.Runtime.InteropServices.GCHandle.Alloc(plugin_name_{plugin_lower}, System.Runtime.InteropServices.GCHandleType.Pinned);\n"
));
out.push_str(&format!(
" var contractHandle_{plugin_lower} = System.Runtime.InteropServices.GCHandle.Alloc(contract_name_{plugin_lower}, System.Runtime.InteropServices.GCHandleType.Pinned);\n"
));
out.push_str(" try {\n");
out.push_str(&format!(" fixed (GuestContractInterface* interfacePtr_{plugin_lower} = &{}Interfaces.{plugin_upper}_INTERFACE) {{\n", plugin_pascal));
out.push_str(&format!(
" var desc_{plugin_lower} = new PluginDescriptor {{\n"
));
out.push_str(&format!(
" Name = new StringView {{ Ptr = nameHandle_{plugin_lower}.AddrOfPinnedObject(), Len = (nuint)plugin_name_{plugin_lower}.Length }},\n"
));
out.push_str(&format!(
" ContractName = new StringView {{ Ptr = contractHandle_{plugin_lower}.AddrOfPinnedObject(), Len = (nuint)contract_name_{plugin_lower}.Length }},\n"
));
out.push_str(&format!(" Version = new Polyplug.Abi.Version {{ Major = {major}u, Minor = {minor}u, Patch = {patch}u }},\n"));
out.push_str(" };\n");
out.push_str(" var host = (HostApi*)hostPtr;\n");
out.push_str(" var registerFn = (delegate* unmanaged[Cdecl]<IntPtr, PluginDescriptor*, GuestContractInterface*, AbiError*, void>)host->RegisterGuestContract;\n");
out.push_str(&format!(
" AbiError err_{plugin_lower} = default;\n"
));
out.push_str(&format!(
" registerFn(hostPtr, &desc_{plugin_lower}, interfacePtr_{plugin_lower}, &err_{plugin_lower});\n"
));
out.push_str(&format!(" if (err_{plugin_lower}.Code != (uint)AbiErrorCode.Ok) return err_{plugin_lower};\n"));
out.push_str(" }\n");
out.push_str(" } finally {\n");
out.push_str(&format!(
" nameHandle_{plugin_lower}.Free();\n"
));
out.push_str(&format!(
" contractHandle_{plugin_lower}.Free();\n"
));
out.push_str(" }\n");
}
}
}
} else {
for contract in &ir.contracts {
let lower: String = contract.name.replace('.', "_");
let class_name: String = contract_name_to_cs_class(&contract.name);
let upper: String = contract_name_to_upper_snake(&contract.name);
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(" // Register {}\n", contract.name));
out.push_str(&format!(
" var plugin_name_{lower} = System.Text.Encoding.UTF8.GetBytes(\"{lower}_plugin\");\n"
));
let contract_name_full: String =
format!("{}@{}", contract.name, contract.version.major);
out.push_str(&format!(
" var contract_name_{lower} = System.Text.Encoding.UTF8.GetBytes(\"{}\");\n",
contract_name_full
));
out.push_str(&format!(
" var nameHandle_{lower} = System.Runtime.InteropServices.GCHandle.Alloc(plugin_name_{lower}, System.Runtime.InteropServices.GCHandleType.Pinned);\n"
));
out.push_str(&format!(
" var contractHandle_{lower} = System.Runtime.InteropServices.GCHandle.Alloc(contract_name_{lower}, System.Runtime.InteropServices.GCHandleType.Pinned);\n"
));
out.push_str(" try {\n");
out.push_str(&format!(" fixed (GuestContractInterface* interfacePtr_{lower} = &{class_name}Interfaces.{upper}_INTERFACE) {{\n"));
out.push_str(&format!(
" var desc_{lower} = new PluginDescriptor {{\n"
));
out.push_str(&format!(
" Name = new StringView {{ Ptr = nameHandle_{lower}.AddrOfPinnedObject(), Len = (nuint)plugin_name_{lower}.Length }},\n"
));
out.push_str(&format!(
" ContractName = new StringView {{ Ptr = contractHandle_{lower}.AddrOfPinnedObject(), Len = (nuint)contract_name_{lower}.Length }},\n"
));
out.push_str(&format!(" Version = new Polyplug.Abi.Version {{ Major = {major}u, Minor = {minor}u, Patch = {patch}u }},\n"));
out.push_str(" };\n");
out.push_str(" var host = (HostApi*)hostPtr;\n");
out.push_str(" var registerFn = (delegate* unmanaged[Cdecl]<IntPtr, PluginDescriptor*, GuestContractInterface*, AbiError*, void>)host->RegisterGuestContract;\n");
out.push_str(&format!(
" AbiError err_{lower} = default;\n"
));
out.push_str(&format!(
" registerFn(hostPtr, &desc_{lower}, interfacePtr_{lower}, &err_{lower});\n"
));
out.push_str(&format!(" if (err_{lower}.Code != (uint)AbiErrorCode.Ok) return err_{lower};\n"));
out.push_str(" }\n");
out.push_str(" } finally {\n");
out.push_str(&format!(" nameHandle_{lower}.Free();\n"));
out.push_str(&format!(" contractHandle_{lower}.Free();\n"));
out.push_str(" }\n");
}
}
out.push_str(" return new AbiError { Code = (uint)AbiErrorCode.Ok };\n");
out.push_str(" } // unsafe\n");
out.push_str(" } catch {\n");
out.push_str(" return new AbiError { Code = (uint)AbiErrorCode.Panic, Message = StringViewHelper.StaticMessage(\"plugin panicked\") };\n");
out.push_str(" } finally {\n");
out.push_str(" System.Threading.Thread.EndThreadAffinity();\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str("}\n");
out
}
fn generate_cs_host_callers(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Host;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System;\n");
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
out.push_str("namespace Polyplug.Generated;\n\n");
if ir.contracts.iter().any(contract_needs_arena) {
emit_cs_call_arena_helpers(&mut out);
}
for contract in &ir.contracts {
let class_name: String = contract_name_to_cs_class(&contract.name);
let caller_name: String = format!("{class_name}Caller");
let fn_count: usize = contract.functions.len();
let needs_arena: bool = contract_needs_arena(contract);
let contract_upper: String = contract.name.to_uppercase().replace(['.', '-'], "_");
out.push_str(&format!("public static class {class_name}Constants {{\n"));
out.push_str(&format!(
" public const ulong {contract_upper}_CONTRACT_ID = 0x{:016X}UL;\n",
contract.contract_id
));
out.push_str(&format!(
" public const uint {contract_upper}_FUNCTION_COUNT = {fn_count}u;\n"
));
out.push_str("}\n\n");
out.push_str(&format!(
"/// <summary>\n/// Host caller for contract `{}` (id=0x{:016X})\n/// Instance-based RAII wrapper with automatic cleanup via IDisposable.\n/// </summary>\n",
contract.name, contract.contract_id
));
out.push_str(&format!(
"public sealed unsafe class {caller_name} : IDisposable {{\n"
));
out.push_str(" private GuestContractInterface* _interface;\n");
out.push_str(" private GuestContractInstance _instance;\n");
out.push_str(" private readonly HostApi* _host;\n");
out.push_str(" private bool _disposed;\n");
out.push_str(
" /// <summary>Contract handle, retained so the cache can re-resolve after a\n",
);
out.push_str(
" /// hot-reload (which swaps a new interface into the same slot) or report a\n",
);
out.push_str(" /// gone contract.</summary>\n");
out.push_str(" private readonly GuestContractHandle _handle;\n");
out.push_str(
" /// <summary>Pointer to the runtime's registry revision counter, fetched once\n",
);
out.push_str(
" /// via HostApi.RevisionCounter. Polled directly before each dispatch (one\n",
);
out.push_str(
" /// atomic load, no call into the runtime); IntPtr.Zero when there is no runtime.</summary>\n",
);
out.push_str(" private readonly IntPtr _revisionPtr;\n");
out.push_str(
" /// <summary>Revision value read when the interface was resolved. Compared\n",
);
out.push_str(
" /// before each dispatch against the live counter to detect a reload/unload and\n",
);
out.push_str(
" /// re-resolve, so the cached interface pointer never dangles.</summary>\n",
);
out.push_str(" private ulong _cachedRevision;\n");
if needs_arena {
out.push_str(
" /// <summary>Unmanaged backing buffer for the per-call arena. C-heap\n",
);
out.push_str(
" /// memory (never the managed heap) so cross-boundary data stays off the GC.</summary>\n",
);
out.push_str(" private readonly byte* _arenaBuf;\n");
out.push_str(
" /// <summary>Per-call bump arena over `_arenaBuf`, reset at each arena-backed call.</summary>\n",
);
out.push_str(" private CallArena _arena;\n");
}
out.push('\n');
if needs_arena {
out.push_str(&format!(
" private {caller_name}(GuestContractInterface* iface, GuestContractInstance inst, HostApi* host, GuestContractHandle handle, IntPtr revisionPtr, ulong cachedRevision) {{\n"
));
out.push_str(" _interface = iface;\n");
out.push_str(" _instance = inst;\n");
out.push_str(" _host = host;\n");
out.push_str(" _disposed = false;\n");
out.push_str(" _handle = handle;\n");
out.push_str(" _revisionPtr = revisionPtr;\n");
out.push_str(" _cachedRevision = cachedRevision;\n");
out.push_str(
" // C-heap allocation: cross-boundary arena data must not live on the managed heap.\n",
);
out.push_str(
" _arenaBuf = (byte*)NativeMemory.Alloc(CallArenaOps.CALL_ARENA_BUF_LEN);\n",
);
out.push_str(" _arena = CallArenaOps.New(_arenaBuf, CallArenaOps.CALL_ARENA_BUF_LEN, (IntPtr)host);\n");
out.push_str(" }\n\n");
} else {
out.push_str(&format!(
" private {caller_name}(GuestContractInterface* iface, GuestContractInstance inst, HostApi* host, GuestContractHandle handle, IntPtr revisionPtr, ulong cachedRevision) {{\n"
));
out.push_str(" _interface = iface;\n");
out.push_str(" _instance = inst;\n");
out.push_str(" _host = host;\n");
out.push_str(" _disposed = false;\n");
out.push_str(" _handle = handle;\n");
out.push_str(" _revisionPtr = revisionPtr;\n");
out.push_str(" _cachedRevision = cachedRevision;\n");
out.push_str(" }\n\n");
}
let create_cast: &str = "((delegate* unmanaged[Cdecl]<HostApi*, GuestContractInterface*, void*, GuestContractInstance*, void>)_host->CreateGuestInstance)";
let destroy_cast: &str = "((delegate* unmanaged[Cdecl]<HostApi*, GuestContractInterface*, GuestContractInstance, void>)_host->DestroyGuestInstance)";
out.push_str(
" /// <summary>Factory method - resolves contract and creates instance.</summary>\n",
);
out.push_str(&format!(
" public static {caller_name}? Create(Runtime rt) {{\n"
));
out.push_str(&format!(
" var handle = rt.FindGuestContract({class_name}Constants.{contract_upper}_CONTRACT_ID, 0);\n"
));
out.push_str(" var host = (HostApi*)rt.HostHandle;\n");
out.push_str(
" var iface = (GuestContractInterface*)rt.ResolveGuestContract(handle);\n",
);
out.push_str(" if (iface == null) { return null; }\n");
out.push_str(" var createFn = (delegate* unmanaged[Cdecl]<HostApi*, GuestContractInterface*, void*, GuestContractInstance*, void>)host->CreateGuestInstance;\n");
out.push_str(" GuestContractInstance inst = default;\n");
out.push_str(" createFn(host, iface, null, &inst);\n");
out.push_str(
" var revisionFn = (delegate* unmanaged[Cdecl]<HostApi*, IntPtr>)host->RevisionCounter;\n",
);
out.push_str(" IntPtr revisionPtr = revisionFn(host);\n");
out.push_str(" ulong cachedRevision = revisionPtr == IntPtr.Zero\n");
out.push_str(" ? 0UL\n");
out.push_str(" : System.Threading.Volatile.Read(ref System.Runtime.CompilerServices.Unsafe.AsRef<ulong>((void*)revisionPtr));\n");
out.push_str(&format!(
" return new {caller_name}(iface, inst, host, handle, revisionPtr, cachedRevision);\n"
));
out.push_str(" }\n\n");
out.push_str(" /// <summary>Check if this caller holds a resolved contract interface.</summary>\n");
out.push_str(" public bool IsValid => !_disposed && _interface != null;\n\n");
out.push_str(
" /// <summary>Read the registry revision through the cached pointer — one\n",
);
out.push_str(
" /// acquire atomic load, no call into the runtime. Returns the cached value\n",
);
out.push_str(" /// (\"unchanged\") when there is no counter (IntPtr.Zero).</summary>\n");
out.push_str(" private ulong LiveRevision() {\n");
out.push_str(" if (_revisionPtr == IntPtr.Zero) { return _cachedRevision; }\n");
out.push_str(" return System.Threading.Volatile.Read(ref System.Runtime.CompilerServices.Unsafe.AsRef<ulong>((void*)_revisionPtr));\n");
out.push_str(" }\n\n");
let create_cast_rv: &str = "((delegate* unmanaged[Cdecl]<HostApi*, GuestContractInterface*, void*, GuestContractInstance*, void>)_host->CreateGuestInstance)";
out.push_str(
" /// <summary>Re-resolve the cached interface after the registry changed under\n",
);
out.push_str(
" /// us. A hot-reload swapped a new interface into the same slot, so the retained\n",
);
out.push_str(
" /// handle still resolves — to the new interface; an unload vacated the slot, so\n",
);
out.push_str(
" /// it resolves to null and false is returned (the contract is gone). The old\n",
);
out.push_str(
" /// instance is ABANDONED, never destroyed: its interface and guest-side state are\n",
);
out.push_str(
" /// already epoch-reclaimed, so calling the dead interface's destroy would be UB.</summary>\n",
);
out.push_str(" private bool Revalidate() {\n");
out.push_str(" if (_host == null) { return false; }\n");
out.push_str(
" var resolveFn = (delegate* unmanaged[Cdecl]<HostApi*, GuestContractHandle, GuestContractInterface*>)_host->ResolveGuestContract;\n",
);
out.push_str(" GuestContractInterface* iface = resolveFn(_host, _handle);\n");
out.push_str(" if (iface == null) { return false; }\n");
out.push_str(" GuestContractInstance inst = default;\n");
out.push_str(&format!(
" {create_cast_rv}(_host, iface, null, &inst);\n"
));
out.push_str(" _interface = iface;\n");
out.push_str(" _instance = inst;\n");
out.push_str(" _cachedRevision = LiveRevision();\n");
out.push_str(" return true;\n");
out.push_str(" }\n\n");
out.push_str(
" /// <summary>Reset instance - destroy existing and create new.</summary>\n",
);
out.push_str(" public void Reset() {\n");
out.push_str(
" // If the registry changed under us, the cached interface/instance are stale\n",
);
out.push_str(
" // (a reload/unload reclaimed their backing). Revalidate() abandons the dead\n",
);
out.push_str(
" // instance and builds a fresh one on the current interface — exactly the\n",
);
out.push_str(
" // fresh instance Reset() promises — so defer to it and skip the unsafe destroy.\n",
);
out.push_str(" if (LiveRevision() != _cachedRevision) {\n");
out.push_str(" Revalidate();\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" if (!_disposed) {\n");
out.push_str(&format!(
" {destroy_cast}(_host, _interface, _instance);\n"
));
out.push_str(" }\n");
out.push_str(" GuestContractInstance newInst = default;\n");
out.push_str(&format!(
" {create_cast}(_host, _interface, null, &newInst);\n"
));
out.push_str(" _instance = newInst;\n");
out.push_str(" }\n\n");
out.push_str(
" /// <summary>Dispose pattern - calls destroy_instance on cleanup.</summary>\n",
);
out.push_str(" public void Dispose() {\n");
out.push_str(" if (!_disposed) {\n");
out.push_str(
" // If the registry changed since we resolved, the cached interface and\n",
);
out.push_str(
" // instance are stale — a reload/unload reclaimed their backing — so calling\n",
);
out.push_str(
" // the dead interface's destroy would be UB; the reload/unload already\n",
);
out.push_str(" // reclaimed the instance, so skip the destroy entirely.\n");
out.push_str(" if (LiveRevision() == _cachedRevision) {\n");
out.push_str(&format!(
" {destroy_cast}(_host, _interface, _instance);\n"
));
out.push_str(" _instance.Data = nint.Zero;\n");
out.push_str(" }\n");
if needs_arena {
out.push_str(
" // Free all retained overflow blocks, then the C-heap buffer.\n",
);
out.push_str(" fixed (CallArena* arenaPtr = &_arena) {\n");
out.push_str(" CallArenaOps.FreeAll(arenaPtr);\n");
out.push_str(" }\n");
out.push_str(" NativeMemory.Free(_arenaBuf);\n");
}
out.push_str(" _disposed = true;\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_host_fn_caller(&mut out, func, contract, &class_name);
}
out.push_str("}\n\n");
}
out
}
fn generate_host_fn_caller(
out: &mut String,
func: &ResolvedFunction,
_contract: &ResolvedContract,
contract_struct: &str,
) {
let fn_id: u32 = func.function_id;
let method_name: String = pascal_case(&func.name);
let ret: String = cs_return_type(func);
let has_return: bool = func.returns.is_some();
let params_sig: String = if func.params.is_empty() {
String::new()
} else if needs_arg_pack(&func.params) {
let args_struct: String = format!("{}{}Args", contract_struct, method_name);
format!("ref {args_struct} args")
} else {
let p: &ResolvedParam = &func.params[0];
let cs_ty: String = cs_type_name(&p.ty);
match &p.ty {
ResolvedTypeRef::UserDefined(_) => {
format!("ref {cs_ty} {name}", name = p.name, cs_ty = cs_ty)
}
_ => format!("{cs_ty} {name}", name = p.name, cs_ty = cs_ty),
}
};
let needs_arena: bool = fn_needs_arena(func);
if needs_arena {
out.push_str(
" /// <summary>Returns a value borrowing this caller's arena; it stays valid\n",
);
out.push_str(" /// until the next arena-backed call on this caller.</summary>\n");
}
out.push_str(&format!(
" public {ret} {method_name}({params_sig}) {{\n"
));
out.push_str(" if (_disposed) {\n");
let caller_name: String = format!("{}Caller", contract_struct);
out.push_str(&format!(
" throw new ObjectDisposedException(nameof({caller_name}));\n"
));
out.push_str(" }\n\n");
out.push_str(" if (LiveRevision() != _cachedRevision && !Revalidate()) {\n");
out.push_str(
" throw new InvalidOperationException(\"contract not found after reload\");\n",
);
out.push_str(" }\n\n");
if needs_arena {
out.push_str(
" // Rewind the arena at call start: retained overflow blocks and the primary\n",
);
out.push_str(" // region become available again, invalidating prior views.\n");
out.push_str(" fixed (CallArena* arenaResetPtr = &_arena) {\n");
out.push_str(" CallArenaOps.Reset(arenaResetPtr);\n");
out.push_str(" }\n");
}
out.push_str(" {\n");
if func.params.is_empty() {
out.push_str(" nint argsPtr = nint.Zero;\n");
} else if needs_arg_pack(&func.params) {
out.push_str(" nint argsPtr = (nint)(&args);\n");
} else {
let p: &ResolvedParam = &func.params[0];
match &p.ty {
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" nint argsPtr = (nint)(&{name});\n",
name = p.name
));
}
_ => {
out.push_str(&format!(
" {ty} {name}_arg = {name};\n",
ty = cs_type_name(&p.ty),
name = p.name
));
out.push_str(&format!(
" nint argsPtr = (nint)(&{name}_arg);\n",
name = p.name
));
}
}
}
if has_return {
out.push_str(&format!(" {ret} result = default;\n"));
out.push_str(" nint outPtr = (nint)(&result);\n");
} else {
out.push_str(" nint outPtr = nint.Zero;\n");
}
out.push_str(" AbiError err = default;\n");
out.push_str(" switch (_interface->DispatchType) {\n");
out.push_str(" case DispatchType.Native: {\n");
out.push_str(&format!(
" if ({fn_id}u >= _interface->Dispatch.Native.FunctionCount) {{\n"
));
out.push_str(
" throw new InvalidOperationException(\"function not available\");\n",
);
out.push_str(" }\n");
out.push_str(" nint funcsArray = _interface->Dispatch.Native.Functions;\n");
out.push_str(&format!(
" nint funcPtr = ((nint*)funcsArray)[{fn_id}];\n"
));
out.push_str(" var dispatch = (delegate* unmanaged[Cdecl]<GuestContractInstance, nint, nint, AbiError*, void>)funcPtr;\n");
out.push_str(" dispatch(_instance, argsPtr, outPtr, &err);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType.VirtualMachine: {\n");
out.push_str(" var vmFn = (delegate* unmanaged[Cdecl]<VmLoaderData, GuestContractInstance, uint, nint, nint, CallArena*, AbiError*, void>)_interface->Dispatch.Vm.Call;\n");
if needs_arena {
out.push_str(" fixed (CallArena* arenaPtr = &_arena) {\n");
out.push_str(&format!(
" vmFn(_interface->Dispatch.Vm.LoaderData, _instance, {fn_id}u, argsPtr, outPtr, arenaPtr, &err);\n"
));
out.push_str(" }\n");
} else {
out.push_str(&format!(
" vmFn(_interface->Dispatch.Vm.LoaderData, _instance, {fn_id}u, argsPtr, outPtr, (CallArena*)null, &err);\n"
));
}
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" default:\n");
out.push_str(
" throw new InvalidOperationException(\"unknown dispatch type\");\n",
);
out.push_str(" }\n");
out.push_str(" if (err.Code != (uint)AbiErrorCode.Ok) {\n");
out.push_str(" throw new InvalidOperationException($\"plugin call failed: code={err.Code}\");\n");
out.push_str(" }\n");
if has_return {
out.push_str(" return result;\n");
}
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn generate_host_manifest(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str("# THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n");
out.push_str(
"# Re-generate with: polyplugc generate --api api.toml --lang csharp --out <dir>\n\n",
);
for contract in &ir.contracts {
out.push_str(&format!(
"[[plugin_contract]]\nname = \"{}\"\nversion = \"{}.{}.{}\"\n\n",
contract.name, contract.version.major, contract.version.minor, contract.version.patch
));
}
out
}
fn host_contract_name_to_cs_caller(name: &str) -> String {
let name_without_prefix: &str = name.strip_prefix("host.").unwrap_or(name);
let pascal: String = name_without_prefix
.split('.')
.map(|p: &str| {
let mut chars: core::str::Chars<'_> = p.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("");
if pascal.starts_with("Host") {
pascal + "Contract"
} else {
"Host".to_owned() + &pascal + "Contract"
}
}
fn cs_guest_caller_param_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "bool".to_owned(),
},
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "string".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "byte[]".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "IntPtr".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => format!("ref {}", name),
}
}
fn cs_guest_caller_return_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "bool".to_owned(),
},
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "System.ReadOnlySpan<byte>".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "System.ReadOnlySpan<byte>".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "IntPtr".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn cs_guest_caller_out_local_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "Polyplug.Abi.StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Polyplug.Abi.Buffer".to_owned(),
_ => cs_guest_caller_return_type_name(ty),
}
}
fn cs_guest_caller_return_expr(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
"new System.ReadOnlySpan<byte>((void*)result.Ptr, (int)result.Len)".to_owned()
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
"new System.ReadOnlySpan<byte>((void*)result.Ptr, (int)result.Len)".to_owned()
}
_ => "result".to_owned(),
}
}
fn generate_cs_guest_host_contract_caller(out: &mut String, contract: &ResolvedHostContract) {
let class_name: String = host_contract_name_to_cs_caller(&contract.name);
out.push_str(&format!(
"/// <summary>\n/// Guest caller for host contract `{}` (id=0x{:016X})\n/// Plugins use this class to call host-provided functionality.\n/// </summary>\n",
contract.name, contract.contract_id
));
out.push_str(&format!("public sealed class {} {{\n", class_name));
out.push_str(" private readonly IntPtr _instance;\n");
out.push_str(" private readonly IntPtr _interface;\n");
out.push_str(" // Host pointer captured in FromHost — used for failure logging.\n");
out.push_str(" // No process-wide host storage exists; the pointer flows per caller.\n");
out.push_str(" private readonly IntPtr _host;\n\n");
out.push_str(&format!(
" private {}(IntPtr host, IntPtr instance, IntPtr iface) {{ _host = host; _instance = instance; _interface = iface; }}\n\n",
class_name
));
out.push_str(" /// <summary>Factory method - creates caller from HostApi or null if not found.</summary>\n");
out.push_str(&format!(
" public static {}? FromHost(IntPtr host, uint minVersion = 0) {{\n",
class_name
));
out.push_str(" if (host == IntPtr.Zero) {\n");
out.push_str(" return null;\n");
out.push_str(" }\n");
out.push_str(" unsafe {\n");
out.push_str(" var hostInterface = (HostApi*)host;\n");
out.push_str(" var getHostContractFn = (delegate* unmanaged[Cdecl]<IntPtr, ulong, uint, IntPtr>)hostInterface->GetHostContract;\n");
out.push_str(" var resolveInterfaceFn = (delegate* unmanaged[Cdecl]<IntPtr, ulong, uint, IntPtr>)hostInterface->ResolveHostContractInterface;\n");
out.push_str(&format!(
" var instance = getHostContractFn(host, 0x{:016X}UL, minVersion);\n",
contract.contract_id
));
out.push_str(" if (instance == IntPtr.Zero) {\n");
out.push_str(" return null;\n");
out.push_str(" }\n");
out.push_str(&format!(
" var iface = resolveInterfaceFn(host, 0x{:016X}UL, minVersion);\n",
contract.contract_id
));
out.push_str(" if (iface == IntPtr.Zero) {\n");
out.push_str(" return null;\n");
out.push_str(" }\n");
out.push_str(&format!(
" return new {}(host, instance, iface);\n",
class_name
));
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" /// <summary>Check if caller is valid (interface is non-null).</summary>\n");
out.push_str(" public bool IsValid => _interface != IntPtr.Zero;\n\n");
for func in &contract.functions {
generate_cs_guest_host_contract_method(out, func, &class_name);
}
out.push_str("}\n\n");
}
fn generate_cs_guest_host_contract_method(
out: &mut String,
func: &ResolvedFunction,
class_name: &str,
) {
let fn_id: u32 = func.function_id;
let method_name: String = pascal_case(&func.name);
let return_type: String = match &func.returns {
Some(ty) => cs_guest_caller_return_type_name(ty),
None => "void".to_owned(),
};
let return_expr: String = match &func.returns {
Some(ty) => cs_guest_caller_return_expr(ty),
None => String::new(),
};
let has_return: bool = func.returns.is_some();
let params_str: String = if func.params.is_empty() {
String::new()
} else {
func.params
.iter()
.map(|p: &ResolvedParam| {
let cs_ty: String = cs_guest_caller_param_type_name(&p.ty);
format!("{} {}", cs_ty, p.name)
})
.collect::<Vec<_>>()
.join(", ")
};
out.push_str(&format!(
" /// <summary>Call host contract function `{}` (function_id={})</summary>\n",
func.name, fn_id
));
out.push_str(&format!(
" public {} {}({}) {{\n",
return_type, method_name, params_str
));
let what: String = format!("{}.{}", class_name, method_name);
out.push_str(" if (_interface == IntPtr.Zero) {\n");
out.push_str(&format!(
" HostCallerFailureLog.Log(_host, \"{what}\", (uint)AbiErrorCode.InvalidPointer);\n"
));
if has_return {
out.push_str(&format!(" return default({});\n", return_type));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n\n");
out.push_str(" unsafe {\n");
out.push_str(" var contract = (HostContractInterface*)_interface;\n");
emit_cs_guest_host_contract_args_setup(out, func, class_name);
emit_cs_guest_host_contract_out_setup(out, &func.returns);
out.push_str(" AbiError err = default;\n");
out.push_str(" switch (contract->DispatchType) {\n");
out.push_str(" case DispatchType.Native: {\n");
out.push_str(&format!(
" if ({fn_id}u >= contract->Dispatch.Native.FunctionCount) {{\n"
));
out.push_str(&format!(
" HostCallerFailureLog.Log(_host, \"{what}\", (uint)AbiErrorCode.FunctionNotAvailable);\n"
));
if has_return {
out.push_str(&format!(
" return default({});\n",
return_type
));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n");
out.push_str(&format!(
" var fnPtr = ((IntPtr*)contract->Dispatch.Native.Functions)[{fn_id}u];\n"
));
out.push_str(" var fn_ = (delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr, AbiError*, void>)fnPtr;\n");
out.push_str(" fn_(_instance, argsPtr, outPtr, &err);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType.VirtualMachine: {\n");
out.push_str(
" var vmFn = (delegate* unmanaged[Cdecl]<VmLoaderData, IntPtr, uint, IntPtr, IntPtr, IntPtr, AbiError*, void>)contract->Dispatch.Vm.Call;\n",
);
out.push_str(&format!(
" vmFn(contract->Dispatch.Vm.LoaderData, _instance, {fn_id}u, argsPtr, outPtr, IntPtr.Zero, &err);\n"
));
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" default:\n");
out.push_str(&format!(
" HostCallerFailureLog.Log(_host, \"{what}\", (uint)AbiErrorCode.Generic);\n"
));
if has_return {
out.push_str(&format!(
" return default({});\n",
return_type
));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n\n");
out.push_str(" if (err.Code != (uint)AbiErrorCode.Ok) {\n");
out.push_str(&format!(
" HostCallerFailureLog.Log(_host, \"{what}\", err.Code);\n"
));
if has_return {
out.push_str(&format!(
" return default({});\n",
return_type
));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n\n");
if has_return {
out.push_str(&format!(" return {};\n", return_expr));
}
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn emit_cs_guest_host_contract_args_setup(
out: &mut String,
func: &ResolvedFunction,
class_name: &str,
) {
if func.params.is_empty() {
out.push_str(" var argsPtr = IntPtr.Zero;\n");
return;
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" using var {0}_pin = new PinnedUtf8({0});\n",
param.name
));
out.push_str(&format!(
" var {0}_view = {0}_pin.View;\n",
param.name
));
out.push_str(&format!(
" var argsPtr = (IntPtr)(&{}_view);\n",
param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" var argsPtr = (IntPtr)(&{});\n",
param.name
));
}
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => {
out.push_str(&format!(
" var local_{name} = {name};\n",
name = param.name
));
out.push_str(&format!(
" var argsPtr = (IntPtr)(&local_{});\n",
param.name
));
}
}
return;
}
let func_name_cap: String = pascal_case(&func.name);
let struct_name: String = format!("{}{}Args", class_name, func_name_cap);
for param in &func.params {
if matches!(¶m.ty, ResolvedTypeRef::AbiType(AbiBuiltin::StringView)) {
out.push_str(&format!(
" using var {0}_pin = new PinnedUtf8({0});\n",
param.name
));
}
}
out.push_str(&format!(" var args = new {} {{\n", struct_name));
for param in &func.params {
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" {} = {}_pin.View,\n",
pascal_case(¶m.name),
param.name
));
}
_ => {
out.push_str(&format!(
" {} = {},\n",
pascal_case(¶m.name),
param.name
));
}
}
}
out.push_str(" };\n");
out.push_str(" var argsPtr = (IntPtr)(&args);\n");
}
fn emit_cs_guest_host_contract_out_setup(out: &mut String, returns: &Option<ResolvedTypeRef>) {
if let Some(ret_ty) = returns {
let local_ty: String = cs_guest_caller_out_local_type_name(ret_ty);
out.push_str(&format!(" {} result = default;\n", local_ty));
out.push_str(" var outPtr = (IntPtr)(&result);\n\n");
} else {
out.push_str(" var outPtr = IntPtr.Zero;\n\n");
}
}
fn emit_cs_log_call_failure_helper(out: &mut String) {
out.push_str("/// <summary>\n");
out.push_str("/// Funnels failed guest→host calls through the host logging path\n");
out.push_str("/// (level Error) before the caller returns its default value.\n");
out.push_str("/// No-op when the host pointer is null (PolyplugHost.Log contract).\n");
out.push_str("/// </summary>\n");
out.push_str("internal static class HostCallerFailureLog {\n");
out.push_str(" internal static void Log(IntPtr hostPtr, string what, uint code) {\n");
out.push_str(
" Polyplug.Guest.PolyplugHost.Log(hostPtr, Polyplug.Abi.LogLevel.Error, \"guest.host_caller\", $\"{what} failed: code={code}\");\n",
);
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn generate_cs_guest_host_contracts_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
emit_cs_log_call_failure_helper(&mut out);
for contract in &ir.host_contracts {
let caller_name: String = host_contract_name_to_cs_caller(&contract.name);
for func in &contract.functions {
if needs_arg_pack(&func.params) {
out.push_str(&emit_cs_arg_pack(&caller_name, func));
out.push('\n');
}
}
}
for contract in &ir.host_contracts {
generate_cs_guest_host_contract_caller(&mut out, contract);
}
for contract in &ir.host_contracts {
let class_name: String = host_contract_name_to_cs_caller(&contract.name);
let const_name: String = class_name.to_uppercase() + "_ID";
out.push_str(&format!(
"/// <summary>\n/// Contract ID constant for `{}` (FNV-1a of \"host_contract:{}@{}\")\n/// </summary>\n",
contract.name, contract.name, contract.version.major
));
out.push_str(&format!("public static class {}Constants {{\n", class_name));
out.push_str(&format!(
" public const ulong {} = 0x{:016X}UL;\n",
const_name, contract.contract_id
));
out.push_str("}\n\n");
}
out
}
fn generate_cs_bundle_constants(ir: &ValidatedIr) -> String {
let bundle: &ResolvedBundle = match ir.bundle.as_ref() {
Some(b) => b,
None => return String::from("// ERROR: bundle constants called without bundle IR\n"),
};
let mut out: String = String::new();
out.push_str("// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n");
out.push_str("// Re-generate with: polyplugc generate --bundle bundle.toml --lang csharp --out <dir>\n\n");
out.push_str("public static class BundleConstants {\n");
out.push_str(&format!(
" public static readonly ulong MyBundleId = {}UL;\n",
bundle.bundle_id
));
out.push_str("}\n");
out
}
fn generate_bundle_manifest_csharp(ir: &ValidatedIr) -> String {
let bundle: &ResolvedBundle = match ir.bundle.as_ref() {
Some(b) => b,
None => return String::from("# ERROR: bundle manifest called without bundle IR\n"),
};
let name: &str = &bundle.name;
let version: String = format!(
"{}.{}.{}",
bundle.version.major, bundle.version.minor, bundle.version.patch
);
let mut provides: Vec<String> = bundle
.plugins
.iter()
.flat_map(|p| p.implements.iter().cloned())
.map(|impl_str: String| {
if let Some(at_pos) = impl_str.find('@') {
let contract_name: &str = &impl_str[..at_pos];
let version_part: &str = &impl_str[at_pos + 1..];
if let Some(dot_pos) = version_part.find('.') {
let major: &str = &version_part[..dot_pos];
format!("{}@{}", contract_name, major)
} else {
impl_str
}
} else {
impl_str
}
})
.collect();
provides.sort();
provides.dedup();
let provides_toml: String = if provides.is_empty() {
String::from("[]")
} else {
format!(
"[{}]",
provides
.iter()
.map(|s: &String| format!("\"{}\"", s))
.collect::<Vec<_>>()
.join(", ")
)
};
let provides_set: std::collections::HashSet<String> = provides.iter().cloned().collect();
let fn_count_entries: Vec<String> = ir
.contracts
.iter()
.filter(|c: &&ResolvedContract| {
provides_set.contains(&format!("{}@{}", c.name, c.version.major))
})
.map(|c: &ResolvedContract| {
let fn_count: u32 = c.functions.len() as u32;
format!("\"{}@{}\" = {}", c.name, c.version.major, fn_count)
})
.collect();
let function_count_toml: String = format!("{{ {} }}", fn_count_entries.join(", "));
let reinit: bool = bundle.needs_reinit_on_dep_reload;
let dep_tables: String = super::emit_manifest_dependencies(&bundle.dependencies);
let file_field: String = super::format_manifest_file_field(&bundle.file);
let loader: &str = &bundle.loader;
format!(
"# THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
name = \"{name}\"\n\
id = {bundle_id}\n\
version = \"{version}\"\n\
loader = \"{loader}\"\n\
provides = {provides_toml}\n\
function_count = {function_count_toml}\n\
needs_reinit_on_dep_reload = {reinit}\n\
{file_field}\n\
{dep_tables}",
bundle_id = bundle.bundle_id
)
}
fn host_contract_name_to_cs_interface(name: &str) -> String {
let name_without_prefix: &str = name.strip_prefix("host.").unwrap_or(name);
let pascal: String = name_without_prefix
.split('.')
.map(|p: &str| {
let mut chars: core::str::Chars<'_> = p.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("");
if pascal.starts_with("Host") {
format!("I{}", pascal)
} else {
format!("IHost{}", pascal)
}
}
fn cs_host_param_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "bool".to_owned(),
},
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "string".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "byte[]".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "IntPtr".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => format!("ref {}", name),
}
}
fn cs_host_return_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "bool".to_owned(),
},
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "string".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "byte[]".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "IntPtr".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn generate_cs_host_interface_method(out: &mut String, func: &ResolvedFunction) {
let return_type: String = match &func.returns {
Some(ty) => cs_host_return_type_name(ty),
None => "void".to_owned(),
};
let method_name: String = pascal_case(&func.name);
let params_str: String = if func.params.is_empty() {
String::new()
} else {
func.params
.iter()
.map(|p: &ResolvedParam| {
let cs_ty: String = cs_host_param_type_name(&p.ty);
format!("{} {}", cs_ty, p.name)
})
.collect::<Vec<_>>()
.join(", ")
};
out.push_str(&format!(
" {} {}({});\n",
return_type, method_name, params_str
));
}
fn generate_cs_host_contract_interface(out: &mut String, contract: &ResolvedHostContract) {
let iface_name: String = host_contract_name_to_cs_interface(&contract.name);
out.push_str(&format!(
"/// <summary>\n/// Host interface for contract `{}` (id=0x{:016X})\n/// Hosts implement this interface to provide functionality to plugins.\n/// </summary>\n",
contract.name, contract.contract_id
));
out.push_str(&format!("public interface {} {{\n", iface_name));
for func in &contract.functions {
generate_cs_host_interface_method(out, func);
}
out.push_str("}\n\n");
}
fn generate_cs_host_contracts_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Host;\n");
out.push_str("using Polyplug.Abi;\n\n");
out.push_str("namespace Polyplug.Generated;\n\n");
for contract in &ir.host_contracts {
generate_cs_host_contract_interface(&mut out, contract);
}
for contract in &ir.host_contracts {
let iface_name: String = host_contract_name_to_cs_interface(&contract.name);
let const_name: String = iface_name.to_uppercase().replace('.', "_") + "_CONTRACT_ID";
out.push_str(&format!(
"/// <summary>\n/// Contract ID constant for `{}` (FNV-1a of \"host_contract:{}@{}\")\n/// </summary>\n",
contract.name, contract.name, contract.version.major
));
out.push_str(&format!(
"public static class {}Constants {{\n",
iface_name.trim_start_matches('I')
));
out.push_str(&format!(
" public const ulong {} = 0x{:016X}UL;\n",
const_name, contract.contract_id
));
out.push_str("}\n\n");
}
out
}
fn generate_cs_host_interface_factories_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Host;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System;\n");
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
out.push_str("namespace Polyplug.Generated;\n\n");
out.push_str("public static class InterfaceFactories {\n");
for contract in &ir.host_contracts {
generate_cs_host_interface_factory(&mut out, contract, &ir.enums);
}
out.push_str("}\n");
out
}
fn generate_cs_host_interface_factory(
out: &mut String,
contract: &ResolvedHostContract,
enums: &[EnumDef],
) {
let iface_name: String = host_contract_name_to_cs_interface(&contract.name);
let factory_name: String = format!("Create{}Interface", iface_name.trim_start_matches('I'));
let factory_vm_name: String =
format!("Create{}InterfaceVm", iface_name.trim_start_matches('I'));
let fn_count: usize = contract.functions.len();
let contract_id: u64 = contract.contract_id;
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let singleton: bool = contract.singleton;
let state_class: String = format!("{}HostState", iface_name.trim_start_matches('I'));
let create_stub: String = format!(
"{}_create_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
let destroy_stub: String = format!(
"{}_destroy_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
let version_lit: String = format!(
"new Polyplug.Abi.Version {{ Major = {major}u, Minor = {minor}u, Patch = {patch}u }}",
major = major,
minor = minor,
patch = contract.version.patch
);
let singleton_lit: &str = if singleton { "true" } else { "false" };
for func in &contract.functions {
generate_cs_host_thunk(out, func, &contract.name, &iface_name, enums);
}
out.push_str(&format!(
"private sealed class {} {{ public {} Impl = default!; public GCHandle FunctionsHandle; }}\n\n",
state_class, iface_name
));
out.push_str("[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
"private static unsafe void {}(IntPtr self, IntPtr args, HostContractInstance* outInstance) {{\n",
create_stub
));
out.push_str(" _ = args;\n");
out.push_str(" if (outInstance == null) return;\n");
out.push_str(" var userData = ((HostContractInterface*)self)->UserData;\n");
out.push_str(" *outInstance = new HostContractInstance { Data = userData };\n");
out.push_str("}\n\n");
out.push_str("[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
"private static void {}(IntPtr self, HostContractInstance instance) {{\n",
destroy_stub
));
out.push_str(" _ = self; _ = instance;\n");
out.push_str("}\n\n");
out.push_str(&format!(
"/// <summary>\n/// Create a host contract interface for `{}` with NATIVE dispatch.\n/// </summary>\n",
contract.name
));
out.push_str("/// <remarks>\n");
out.push_str("/// Takes a reference to the implementation and creates an interface.\n");
out.push_str("/// The implementation must implement the interface.\n");
out.push_str("/// </remarks>\n");
out.push_str(&format!(
"public static unsafe HostContractInterface {}<T>(T impl) where T: {} {{\n",
factory_name, iface_name
));
out.push_str(&format!(
" var functions = new IntPtr[{}] {{\n",
fn_count
));
for func in &contract.functions {
let thunk_name: String = format!(
"{}_{}_thunk",
contract.name.replace('.', "_").to_lowercase(),
func.name
);
out.push_str(&format!(
" (IntPtr)(delegate* unmanaged[Cdecl]<IntPtr, IntPtr, IntPtr, AbiError*, void>)&{},\n",
thunk_name
));
}
out.push_str(" };\n\n");
out.push_str(" var functionsHandle = GCHandle.Alloc(functions, GCHandleType.Pinned);\n");
out.push_str(&format!(
" var state = new {} {{ Impl = impl, FunctionsHandle = functionsHandle }};\n",
state_class
));
out.push_str(" var stateHandle = GCHandle.Alloc(state);\n\n");
out.push_str(" return new HostContractInterface {\n");
out.push_str(&format!(" ContractId = 0x{contract_id:016X}UL,\n"));
out.push_str(&format!(" ContractVersion = {version_lit},\n"));
out.push_str(&format!(" Singleton = {singleton_lit},\n"));
out.push_str(" DispatchType = DispatchType.Native,\n");
out.push_str(" Runtime = IntPtr.Zero,\n");
out.push_str(" // UserData carries the per-registration state GCHandle token; the\n");
out.push_str(" // create-instance stub copies it into each instance's Data, and the\n");
out.push_str(" // dispatch thunk recovers the impl from it (no statics).\n");
out.push_str(" UserData = GCHandle.ToIntPtr(stateHandle),\n");
out.push_str(&format!(
" CreateInstance = (IntPtr)(delegate* unmanaged[Cdecl]<IntPtr, IntPtr, HostContractInstance*, void>)&{},\n",
create_stub
));
out.push_str(&format!(
" DestroyInstance = (IntPtr)(delegate* unmanaged[Cdecl]<IntPtr, HostContractInstance, void>)&{},\n",
destroy_stub
));
out.push_str(" Dispatch = new DispatchMechanisms {\n");
out.push_str(" Native = new NativeDispatch {\n");
out.push_str(&format!(" FunctionCount = {fn_count}u,\n"));
out.push_str(" Functions = functionsHandle.AddrOfPinnedObject(),\n");
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n");
out.push_str("}\n\n");
out.push_str(&format!(
"/// <summary>\n/// Create a host contract interface for `{}` with VM dispatch.\n/// </summary>\n",
contract.name
));
out.push_str("/// <remarks>\n");
out.push_str("/// Used when the host implementation is in a VM language (Python, Lua, JS).\n");
out.push_str("/// `dispatchFn` is a `delegate* unmanaged[Cdecl]<...>` cast to IntPtr.\n");
out.push_str("/// </remarks>\n");
out.push_str(&format!(
"public static unsafe HostContractInterface {}(\n",
factory_vm_name
));
out.push_str(" IntPtr loaderData,\n");
out.push_str(" IntPtr dispatchFn\n");
out.push_str(") {\n");
out.push_str(" return new HostContractInterface {\n");
out.push_str(&format!(" ContractId = 0x{contract_id:016X}UL,\n"));
out.push_str(&format!(" ContractVersion = {version_lit},\n"));
out.push_str(&format!(" Singleton = {singleton_lit},\n"));
out.push_str(" DispatchType = DispatchType.VirtualMachine,\n");
out.push_str(" Runtime = IntPtr.Zero,\n");
out.push_str(" UserData = loaderData, // registrant-owned VM bridge data\n");
out.push_str(&format!(
" CreateInstance = (IntPtr)(delegate* unmanaged[Cdecl]<IntPtr, IntPtr, HostContractInstance*, void>)&{},\n",
create_stub
));
out.push_str(&format!(
" DestroyInstance = (IntPtr)(delegate* unmanaged[Cdecl]<IntPtr, HostContractInstance, void>)&{},\n",
destroy_stub
));
out.push_str(" Dispatch = new DispatchMechanisms {\n");
out.push_str(" Vm = new VmDispatch {\n");
out.push_str(" Call = dispatchFn,\n");
out.push_str(" LoaderData = new VmLoaderData { Data = loaderData },\n");
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n");
out.push_str("}\n\n");
}
fn generate_cs_host_thunk(
out: &mut String,
func: &ResolvedFunction,
contract_name: &str,
iface_name: &str,
enums: &[EnumDef],
) {
let thunk_name: String = format!(
"{}_{}_thunk",
contract_name.replace('.', "_").to_lowercase(),
func.name
);
let has_return: bool = func.returns.is_some();
let pack_struct: String = format!(
"{}{}Args",
iface_name.trim_start_matches('I'),
pascal_case(&func.name)
);
if func.params.len() > 1 {
out.push_str("[StructLayout(LayoutKind.Sequential)]\n");
out.push_str(&format!("private struct {} {{\n", pack_struct));
for param in &func.params {
let ty_name: String = cs_host_abi_type_name(¶m.ty);
out.push_str(&format!(
" public {} {};\n",
ty_name,
pascal_case(¶m.name)
));
}
out.push_str("}\n\n");
}
out.push_str("[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]\n");
out.push_str(&format!(
"private static unsafe void {}(IntPtr implPtr, IntPtr argsPtr, IntPtr outPtr, AbiError* outErr) {{\n",
thunk_name
));
out.push_str(" if (outErr == null) return;\n");
out.push_str(" if (implPtr == IntPtr.Zero) {\n");
out.push_str(" *outErr = new AbiError { Code = (uint)AbiErrorCode.InvalidPointer };\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" try {\n");
out.push_str(&format!(
" var state = ({}HostState)GCHandle.FromIntPtr(implPtr).Target!;\n",
iface_name.trim_start_matches('I')
));
out.push_str(" var impl = state.Impl;\n");
if !func.params.is_empty() {
generate_cs_host_thunk_args(out, func, &pack_struct, enums);
} else {
out.push_str(" _ = argsPtr;\n");
}
generate_cs_host_thunk_call(out, func, has_return);
if has_return {
out.push_str(" // SAFETY: outPtr is a valid pointer per ABI contract.\n");
out.push_str(" unsafe { Marshal.StructureToPtr(result, outPtr, false); }\n");
} else {
out.push_str(" _ = outPtr;\n");
}
out.push_str(" *outErr = new AbiError { Code = (uint)AbiErrorCode.Ok };\n");
out.push_str(" } catch (Exception ex) {\n");
out.push_str(" var msg = StringViewHelper.StaticMessage(ex.Message);\n");
out.push_str(
" *outErr = new AbiError { Code = (uint)AbiErrorCode.Panic, Message = msg };\n",
);
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn generate_cs_host_thunk_args(
out: &mut String,
func: &ResolvedFunction,
pack_struct: &str,
enums: &[EnumDef],
) {
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let ty_name: String = cs_host_abi_type_name(¶m.ty);
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" var {}_sv = Marshal.PtrToStructure<StringView>(argsPtr);\n",
param.name
));
out.push_str(&format!(
" var {} = StringViewHelper.ToString({}_sv);\n",
param.name, param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" var {} = Marshal.PtrToStructure<Buffer>(argsPtr);\n",
param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
match cs_enum_for_type(¶m.ty, enums) {
Some(e) => {
out.push_str(&format!(
" var {} = ({})Marshal.PtrToStructure<{}>(argsPtr);\n",
param.name,
ty_name,
e.repr.cs_name()
));
}
None => {
out.push_str(&format!(
" var {} = Marshal.PtrToStructure<{}>(argsPtr);\n",
param.name, ty_name
));
}
}
}
_ => {
out.push_str(&format!(
" var {} = Marshal.PtrToStructure<{}>(argsPtr);\n",
param.name, ty_name
));
}
}
} else {
out.push_str(&format!(
" var packed = Marshal.PtrToStructure<{}>(argsPtr);\n",
pack_struct
));
for param in &func.params {
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" var {} = StringViewHelper.ToString(packed.{});\n",
param.name,
pascal_case(¶m.name)
));
}
_ => {
out.push_str(&format!(
" var {} = packed.{};\n",
param.name,
pascal_case(¶m.name)
));
}
}
}
}
}
fn generate_cs_host_thunk_call(out: &mut String, func: &ResolvedFunction, has_return: bool) {
let call_args: String = if func.params.is_empty() {
String::new()
} else {
func.params
.iter()
.map(|p: &ResolvedParam| match &p.ty {
ResolvedTypeRef::UserDefined(_) => format!("ref {}", p.name),
_ => p.name.clone(),
})
.collect::<Vec<_>>()
.join(", ")
};
if has_return {
out.push_str(&format!(
" var result = impl.{}({});\n",
pascal_case(&func.name),
call_args
));
} else {
out.push_str(&format!(
" impl.{}({});\n",
pascal_case(&func.name),
call_args
));
}
}
fn cs_host_abi_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => match p {
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::Bool => "byte".to_owned(),
},
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Buffer".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "IntPtr".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn cs_enum_for_type<'a>(ty: &ResolvedTypeRef, enums: &'a [EnumDef]) -> Option<&'a EnumDef> {
match ty {
ResolvedTypeRef::UserDefined(name) => enums.iter().find(|e: &&EnumDef| &e.name == name),
_ => None,
}
}
fn generate_cs_peer_callers_file(ir: &ValidatedIr, peers: &[&ResolvedContract]) -> String {
let mut out: String = String::new();
out.push_str(CS_HEADER);
out.push_str("using Polyplug.Guest;\n");
out.push_str("using Polyplug.Abi;\n");
out.push_str("using System;\n");
out.push_str("using System.Runtime.CompilerServices;\n");
out.push_str("using System.Runtime.InteropServices;\n\n");
out.push_str("namespace Polyplug.Generated;\n\n");
let any_needs_arena: bool = peers
.iter()
.any(|c: &&ResolvedContract| contract_needs_arena(c));
if any_needs_arena {
emit_cs_call_arena_helpers(&mut out);
}
for contract in peers {
let class_name: String = contract_name_to_cs_class(&contract.name);
let peer_name: String = format!("{}Peer", class_name);
let min_ver: u32 = peer_min_version(ir, contract.contract_id);
let needs_arena: bool = contract_needs_arena(contract);
let contract_upper: String = contract_name_to_upper_snake(&contract.name);
out.push_str(&format!(
"/// <summary>\n/// Guest-side peer caller for contract `{}` (id=0x{:016X}).\n\
/// Dispatches directly through the cached interface (no host-mediated round-trip);\n\
/// the host pointer is passed explicitly to Resolve(hostPtr) by the caller.\n\
/// Returns raw ABI types — do not convert StringView/Buffer to managed strings.\n\
/// </summary>\n",
contract.name, contract.contract_id
));
out.push_str(&format!(
"public sealed unsafe class {peer_name} : IDisposable {{\n"
));
out.push_str(" private GuestContractInterface* _interface;\n");
out.push_str(" private GuestContractInstance _instance;\n");
out.push_str(" private HostApi* _host;\n");
out.push_str(" private bool _disposed;\n");
out.push_str(
" /// <summary>Peer contract handle, retained so the cache can re-resolve after a\n",
);
out.push_str(
" /// hot-reload (which swaps a new interface into the same slot) or report a\n",
);
out.push_str(" /// gone peer.</summary>\n");
out.push_str(" private readonly GuestContractHandle _handle;\n");
out.push_str(
" /// <summary>Pointer to the runtime's registry revision counter, fetched once\n",
);
out.push_str(
" /// via HostApi.RevisionCounter. Polled directly before each dispatch (one\n",
);
out.push_str(
" /// atomic load, no call into the runtime); IntPtr.Zero when there is no runtime.</summary>\n",
);
out.push_str(" private readonly IntPtr _revisionPtr;\n");
out.push_str(
" /// <summary>Revision value read when the peer was resolved. Compared before each\n",
);
out.push_str(
" /// dispatch against the live counter to detect a reload/unload and re-resolve, so\n",
);
out.push_str(" /// the cached interface pointer and instance never dangle.</summary>\n");
out.push_str(" private ulong _cachedRevision;\n");
if needs_arena {
out.push_str(
" /// <summary>Unmanaged backing buffer for the per-call arena. C-heap\n",
);
out.push_str(
" /// memory (never the managed heap) so cross-boundary data stays off the GC.</summary>\n",
);
out.push_str(" private readonly byte* _arenaBuf;\n");
out.push_str(
" /// <summary>Per-call bump arena over `_arenaBuf`, reset at each arena-backed call.</summary>\n",
);
out.push_str(" private CallArena _arena;\n");
}
out.push('\n');
out.push_str(&format!(
" private {peer_name}(GuestContractInterface* iface, GuestContractInstance inst, HostApi* host, GuestContractHandle handle, IntPtr revisionPtr, ulong cachedRevision) {{\n"
));
out.push_str(" _interface = iface;\n");
out.push_str(" _instance = inst;\n");
out.push_str(" _host = host;\n");
out.push_str(" _disposed = false;\n");
out.push_str(" _handle = handle;\n");
out.push_str(" _revisionPtr = revisionPtr;\n");
out.push_str(" _cachedRevision = cachedRevision;\n");
if needs_arena {
out.push_str(
" // C-heap allocation: cross-boundary arena data must not live on the managed heap.\n",
);
out.push_str(
" _arenaBuf = (byte*)NativeMemory.Alloc(CallArenaOps.CALL_ARENA_BUF_LEN);\n",
);
out.push_str(" _arena = CallArenaOps.New(_arenaBuf, CallArenaOps.CALL_ARENA_BUF_LEN, (IntPtr)host);\n");
}
out.push_str(" }\n\n");
out.push_str(" /// <summary>Resolve the peer contract and create a caller instance.\n");
out.push_str(
" /// <paramref name=\"hostPtr\"/> is the HostApi pointer the author factory\n",
);
out.push_str(
" /// received — no process-wide host storage exists. Returns null when the\n",
);
out.push_str(
" /// host pointer is null, or when the contract is not found/resolved.</summary>\n",
);
out.push_str(&format!(
" public static {peer_name}? Resolve(IntPtr hostPtr) {{\n"
));
out.push_str(" if (hostPtr == IntPtr.Zero) { return null; }\n");
out.push_str(" var host = (HostApi*)hostPtr;\n");
out.push_str(&format!(
" // FindGuestContract: find contract 0x{:016X}UL, min major = {min_ver}u\n",
contract.contract_id
));
out.push_str(" var findFn = (delegate* unmanaged[Cdecl]<IntPtr, ulong, uint, GuestContractHandle>)host->FindGuestContract;\n");
out.push_str(&format!(
" GuestContractHandle handle = findFn(hostPtr, 0x{:016X}UL, {min_ver}u);\n",
contract.contract_id
));
out.push_str(" var resolveFn = (delegate* unmanaged[Cdecl]<IntPtr, GuestContractHandle, GuestContractInterface*>)host->ResolveGuestContract;\n");
out.push_str(" GuestContractInterface* iface = resolveFn(hostPtr, handle);\n");
out.push_str(" if (iface == null) { return null; }\n");
out.push_str(" var createFn = (delegate* unmanaged[Cdecl]<IntPtr, GuestContractInterface*, void*, GuestContractInstance*, void>)host->CreateGuestInstance;\n");
out.push_str(" GuestContractInstance inst = default;\n");
out.push_str(" createFn(hostPtr, iface, null, &inst);\n");
out.push_str(
" var revisionFn = (delegate* unmanaged[Cdecl]<IntPtr, IntPtr>)host->RevisionCounter;\n",
);
out.push_str(" IntPtr revisionPtr = revisionFn(hostPtr);\n");
out.push_str(" ulong cachedRevision = revisionPtr == IntPtr.Zero\n");
out.push_str(" ? 0UL\n");
out.push_str(" : System.Threading.Volatile.Read(ref System.Runtime.CompilerServices.Unsafe.AsRef<ulong>((void*)revisionPtr));\n");
out.push_str(&format!(
" return new {peer_name}(iface, inst, host, handle, revisionPtr, cachedRevision);\n"
));
out.push_str(" }\n\n");
out.push_str(
" /// <summary>True while this peer holds a live interface and has not been disposed.</summary>\n",
);
out.push_str(" public bool IsValid => !_disposed && _interface != null;\n\n");
out.push_str(
" /// <summary>Read the registry revision through the cached pointer — one\n",
);
out.push_str(
" /// acquire atomic load, no call into the runtime. Returns the cached value\n",
);
out.push_str(" /// (\"unchanged\") when there is no counter (IntPtr.Zero).</summary>\n");
out.push_str(" private ulong LiveRevision() {\n");
out.push_str(" if (_revisionPtr == IntPtr.Zero) { return _cachedRevision; }\n");
out.push_str(" return System.Threading.Volatile.Read(ref System.Runtime.CompilerServices.Unsafe.AsRef<ulong>((void*)_revisionPtr));\n");
out.push_str(" }\n\n");
out.push_str(
" /// <summary>Re-resolve the cached peer interface after the registry changed\n",
);
out.push_str(
" /// under us. A hot-reload swapped a new interface into the same slot, so the\n",
);
out.push_str(
" /// retained handle still resolves — to the new interface; an unload vacated the\n",
);
out.push_str(
" /// slot, so it resolves to null and false is returned (the peer is gone). The old\n",
);
out.push_str(
" /// instance is ABANDONED, never destroyed: its interface and guest-side state are\n",
);
out.push_str(
" /// already epoch-reclaimed, so calling the dead interface's destroy would be UB.</summary>\n",
);
out.push_str(" private bool Revalidate() {\n");
out.push_str(" if (_host == null) { return false; }\n");
out.push_str(
" var resolveFn = (delegate* unmanaged[Cdecl]<IntPtr, GuestContractHandle, GuestContractInterface*>)_host->ResolveGuestContract;\n",
);
out.push_str(
" GuestContractInterface* iface = resolveFn((IntPtr)_host, _handle);\n",
);
out.push_str(" if (iface == null) { return false; }\n");
out.push_str(
" var createFn = (delegate* unmanaged[Cdecl]<IntPtr, GuestContractInterface*, void*, GuestContractInstance*, void>)_host->CreateGuestInstance;\n",
);
out.push_str(" GuestContractInstance inst = default;\n");
out.push_str(" createFn((IntPtr)_host, iface, null, &inst);\n");
out.push_str(" _interface = iface;\n");
out.push_str(" _instance = inst;\n");
out.push_str(" _cachedRevision = LiveRevision();\n");
out.push_str(" return true;\n");
out.push_str(" }\n\n");
let destroy_cast: &str = "((delegate* unmanaged[Cdecl]<IntPtr, GuestContractInterface*, GuestContractInstance, void>)_host->DestroyGuestInstance)";
out.push_str(
" /// <summary>Dispose pattern — calls destroy_guest_instance and frees arena memory.</summary>\n",
);
out.push_str(" public void Dispose() {\n");
out.push_str(" if (!_disposed) {\n");
out.push_str(
" // If the registry changed since we resolved, the cached interface and\n",
);
out.push_str(
" // instance are stale — a reload/unload reclaimed their backing — so calling\n",
);
out.push_str(
" // the dead interface's destroy would be UB; the reload/unload already\n",
);
out.push_str(" // reclaimed the instance, so skip the destroy entirely.\n");
out.push_str(" if (LiveRevision() == _cachedRevision) {\n");
out.push_str(&format!(
" {destroy_cast}((IntPtr)_host, _interface, _instance);\n"
));
out.push_str(" _instance.Data = nint.Zero;\n");
out.push_str(" }\n");
if needs_arena {
out.push_str(
" // Free all retained overflow blocks, then the C-heap buffer.\n",
);
out.push_str(" fixed (CallArena* arenaPtr = &_arena) {\n");
out.push_str(" CallArenaOps.FreeAll(arenaPtr);\n");
out.push_str(" }\n");
out.push_str(" NativeMemory.Free(_arenaBuf);\n");
}
out.push_str(" _disposed = true;\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_peer_fn_caller_cs(&mut out, func, &peer_name, &contract_upper, needs_arena);
}
out.push_str("}\n\n");
}
out
}
fn generate_peer_fn_caller_cs(
out: &mut String,
func: &ResolvedFunction,
peer_name: &str,
_contract_upper: &str,
peer_needs_arena: bool,
) {
let fn_id: u32 = func.function_id;
let method_name: String = pascal_case(&func.name);
let ret: String = cs_return_type(func);
let has_return: bool = func.returns.is_some();
let needs_arena: bool = fn_needs_arena(func);
let params_sig: String = if func.params.is_empty() {
String::new()
} else if needs_arg_pack(&func.params) {
let args_struct: String = format!("{}{}{}", peer_name, method_name, "Args");
format!("ref {args_struct} args")
} else {
let p: &ResolvedParam = &func.params[0];
let cs_ty: String = cs_type_name(&p.ty);
match &p.ty {
ResolvedTypeRef::UserDefined(_) => {
format!("ref {cs_ty} {name}", name = p.name, cs_ty = cs_ty)
}
_ => format!("{cs_ty} {name}", name = p.name, cs_ty = cs_ty),
}
};
if needs_arena {
out.push_str(
" /// <summary>Returns a value borrowing this peer's arena; it stays valid\n",
);
out.push_str(" /// until the next arena-backed call on this peer.</summary>\n");
}
out.push_str(&format!(
" public {ret} {method_name}({params_sig}) {{\n"
));
out.push_str(" if (_disposed) {\n");
out.push_str(&format!(
" throw new ObjectDisposedException(nameof({peer_name}));\n"
));
out.push_str(" }\n\n");
out.push_str(" if (LiveRevision() != _cachedRevision && !Revalidate()) {\n");
out.push_str(
" throw new InvalidOperationException(\"peer not found after reload\");\n",
);
out.push_str(" }\n\n");
if needs_arena && peer_needs_arena {
out.push_str(
" // Rewind the arena at call start: retained overflow blocks and the\n",
);
out.push_str(
" // primary region become available again, invalidating prior views.\n",
);
out.push_str(" fixed (CallArena* arenaResetPtr = &_arena) {\n");
out.push_str(" CallArenaOps.Reset(arenaResetPtr);\n");
out.push_str(" }\n");
}
out.push_str(" {\n");
if func.params.is_empty() {
out.push_str(" nint argsPtr = nint.Zero;\n");
} else if needs_arg_pack(&func.params) {
out.push_str(" nint argsPtr = (nint)(&args);\n");
} else {
let p: &ResolvedParam = &func.params[0];
match &p.ty {
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" nint argsPtr = (nint)(&{name});\n",
name = p.name
));
}
_ => {
out.push_str(&format!(
" {ty} {name}_arg = {name};\n",
ty = cs_type_name(&p.ty),
name = p.name
));
out.push_str(&format!(
" nint argsPtr = (nint)(&{name}_arg);\n",
name = p.name
));
}
}
}
if has_return {
out.push_str(&format!(" {ret} result = default;\n"));
out.push_str(" nint outPtr = (nint)(&result);\n");
} else {
out.push_str(" nint outPtr = nint.Zero;\n");
}
out.push_str(" AbiError err = default;\n");
out.push_str(" switch (_interface->DispatchType) {\n");
out.push_str(" case DispatchType.Native: {\n");
out.push_str(&format!(
" if ({fn_id}u >= _interface->Dispatch.Native.FunctionCount) {{\n"
));
out.push_str(
" throw new InvalidOperationException(\"function not available\");\n",
);
out.push_str(" }\n");
out.push_str(" nint funcsArray = _interface->Dispatch.Native.Functions;\n");
out.push_str(&format!(
" nint funcPtr = ((nint*)funcsArray)[{fn_id}];\n"
));
out.push_str(" var dispatch = (delegate* unmanaged[Cdecl]<GuestContractInstance, nint, nint, AbiError*, void>)funcPtr;\n");
out.push_str(" dispatch(_instance, argsPtr, outPtr, &err);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType.VirtualMachine: {\n");
out.push_str(" var vmFn = (delegate* unmanaged[Cdecl]<VmLoaderData, GuestContractInstance, uint, nint, nint, CallArena*, AbiError*, void>)_interface->Dispatch.Vm.Call;\n");
if needs_arena && peer_needs_arena {
out.push_str(" fixed (CallArena* arenaPtr = &_arena) {\n");
out.push_str(&format!(
" vmFn(_interface->Dispatch.Vm.LoaderData, _instance, {fn_id}u, argsPtr, outPtr, arenaPtr, &err);\n"
));
out.push_str(" }\n");
} else {
out.push_str(&format!(
" vmFn(_interface->Dispatch.Vm.LoaderData, _instance, {fn_id}u, argsPtr, outPtr, (CallArena*)null, &err);\n"
));
}
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" default:\n");
out.push_str(
" throw new InvalidOperationException(\"unknown dispatch type\");\n",
);
out.push_str(" }\n");
out.push_str(" if (err.Code != (uint)AbiErrorCode.Ok) {\n");
out.push_str(" throw new InvalidOperationException($\"peer call failed: code={{err.Code}}\");\n");
out.push_str(" }\n");
if has_return {
out.push_str(" return result;\n");
}
out.push_str(" }\n");
out.push_str(" }\n\n");
}
impl CodeGenerator for CSharpGenerator {
fn generate_host(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
files.files.push(GeneratedFile {
path: PathBuf::from("host/Types.cs"),
content: generate_cs_host_types_file(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("host/Callers.cs"),
content: generate_cs_host_callers(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("host/manifest.toml"),
content: generate_host_manifest(ir),
force_regenerate: true,
});
if !ir.host_contracts.is_empty() {
files.files.push(GeneratedFile {
path: PathBuf::from("host/Contracts.cs"),
content: generate_cs_host_contracts_file(ir),
force_regenerate: false,
});
}
if !ir.host_contracts.is_empty() {
files.files.push(GeneratedFile {
path: PathBuf::from("host/InterfaceFactories.cs"),
content: generate_cs_host_interface_factories_file(ir),
force_regenerate: false,
});
}
Ok(())
}
fn generate_guest(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
files.files.push(GeneratedFile {
path: PathBuf::from("guest/Types.cs"),
content: generate_cs_types_file(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("guest/Contracts.cs"),
content: generate_cs_guest_contracts(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("guest/Interfaces.cs"),
content: generate_cs_guest_interfaces(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("guest/Init.cs"),
content: generate_cs_guest_init(ir),
force_regenerate: true,
});
if ir.bundle.is_some() {
files.files.push(GeneratedFile {
path: PathBuf::from("guest/BundleConstants.cs"),
content: generate_cs_bundle_constants(ir),
force_regenerate: false,
});
files.files.push(GeneratedFile {
path: PathBuf::from("manifest.toml"),
content: generate_bundle_manifest_csharp(ir),
force_regenerate: true,
});
}
if !ir.host_contracts.is_empty() {
files.files.push(GeneratedFile {
path: PathBuf::from("guest/HostContracts.cs"),
content: generate_cs_guest_host_contracts_file(ir),
force_regenerate: false,
});
}
let peer_contracts: Vec<&ResolvedContract> = collect_peer_contracts(ir);
if !peer_contracts.is_empty() {
files.files.push(GeneratedFile {
path: PathBuf::from("guest/PeerCallers.cs"),
content: generate_cs_peer_callers_file(ir, &peer_contracts),
force_regenerate: false,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use crate::ir::*;
#[test]
fn generate_cs_enum_non_bitflag() {
let e: EnumDef = EnumDef {
name: "PixelFormat".to_owned(),
repr: ReprType::U32,
bitflag: false,
variants: vec![
EnumVariant {
name: "Unknown".to_owned(),
value: "0".to_owned(),
},
EnumVariant {
name: "Rgba8".to_owned(),
value: "1".to_owned(),
},
],
};
let out: String = generate_cs_enum(&e);
assert!(
out.contains("public enum PixelFormat : uint"),
"missing enum def: {out}"
);
assert!(out.contains("Unknown = 0"), "missing Unknown: {out}");
assert!(
!out.contains("[Flags]"),
"non-bitflag should not have [Flags]: {out}"
);
}
#[test]
fn generate_cs_enum_bitflag() {
let e: EnumDef = EnumDef {
name: "ImageFlags".to_owned(),
repr: ReprType::U32,
bitflag: true,
variants: vec![
EnumVariant {
name: "None".to_owned(),
value: "0".to_owned(),
},
EnumVariant {
name: "Compressed".to_owned(),
value: "1".to_owned(),
},
],
};
let out: String = generate_cs_enum(&e);
assert!(
out.contains("[Flags]"),
"bitflag should have [Flags]: {out}"
);
assert!(
out.contains("public enum ImageFlags : uint"),
"missing enum def: {out}"
);
}
#[test]
fn generate_cs_guest_init_uses_plugin_class() {
let ir: ValidatedIr = ValidatedIr {
contracts: vec![ResolvedContract {
name: "test.check".to_owned(),
contract_id: 0x1234567890ABCDEFu64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
types: vec![],
enums: vec![],
bundle: None,
};
let out: String = generate_cs_guest_init(&ir);
assert!(
out.contains("public static class Plugin"),
"Init.cs must use 'Plugin' class name: {out}"
);
assert!(
!out.contains("PolyplugInitializer"),
"Init.cs must NOT use 'PolyplugInitializer': {out}"
);
assert!(
!out.contains("void* args"),
"Init.cs must not have void* params: {out}"
);
}
#[test]
fn generate_cs_guest_interfaces_no_unsafe_struct() {
let ir: ValidatedIr = ValidatedIr {
contracts: vec![ResolvedContract {
name: "test.check".to_owned(),
contract_id: 0x1234567890ABCDEFu64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
types: vec![],
enums: vec![],
bundle: None,
};
let out: String = generate_cs_guest_interfaces(&ir);
assert!(
!out.contains("unsafe struct"),
"Interfaces.cs must not contain 'unsafe struct': {out}"
);
}
#[test]
fn host_contract_name_to_cs_interface_conversion() {
assert_eq!(
host_contract_name_to_cs_interface("host.logger"),
"IHostLogger"
);
assert_eq!(
host_contract_name_to_cs_interface("host.fs.reader"),
"IHostFsReader"
);
assert_eq!(
host_contract_name_to_cs_interface("host.HostLogger"),
"IHostLogger"
);
assert_eq!(host_contract_name_to_cs_interface("logger"), "IHostLogger");
}
#[test]
fn cs_host_param_type_name_mappings() {
assert_eq!(
cs_host_param_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint"
);
assert_eq!(
cs_host_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"string"
);
assert_eq!(
cs_host_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"byte[]"
);
assert_eq!(
cs_host_param_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"ref MyStruct"
);
}
#[test]
fn cs_host_return_type_name_mappings() {
assert_eq!(
cs_host_return_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint"
);
assert_eq!(
cs_host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"string"
);
assert_eq!(
cs_host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"byte[]"
);
assert_eq!(
cs_host_return_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"MyStruct"
);
}
#[test]
fn generate_cs_host_contract_interface_produces_interface() {
let contract: ResolvedHostContract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![
ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
},
ResolvedFunction {
name: "logf".to_owned(),
function_id: 1,
params: vec![
ResolvedParam {
name: "level".to_owned(),
ty: ResolvedTypeRef::Primitive(PrimitiveType::U32),
},
ResolvedParam {
name: "format".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
},
],
returns: None,
},
],
};
let mut out: String = String::new();
generate_cs_host_contract_interface(&mut out, &contract);
assert!(
out.contains("public interface IHostLogger"),
"missing interface: {out}"
);
assert!(
out.contains("void Log(string message)"),
"missing Log method: {out}"
);
assert!(
out.contains("void Logf(uint level, string format)"),
"missing Logf method: {out}"
);
}
#[test]
fn generate_cs_host_contracts_file_produces_file() {
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
}],
bundle: None,
};
let out: String = generate_cs_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("public interface IHostLogger"),
"missing interface: {out}"
);
assert!(
out.contains("HOSTLOGGER_CONTRACT_ID"),
"missing constant: {out}"
);
assert!(
out.contains("namespace Polyplug.Generated"),
"missing namespace: {out}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_contracts_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![],
returns: None,
}],
}],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
names.contains(&"host/Contracts.cs".to_owned()),
"missing host/Contracts.cs: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_contracts_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
!names.contains(&"host/Contracts.cs".to_owned()),
"unexpected host/Contracts.cs: {names:?}"
);
}
#[test]
fn host_contract_name_to_cs_caller_conversion() {
assert_eq!(
host_contract_name_to_cs_caller("host.logger"),
"HostLoggerContract"
);
assert_eq!(
host_contract_name_to_cs_caller("host.fs.reader"),
"HostFsReaderContract"
);
assert_eq!(
host_contract_name_to_cs_caller("host.HostLogger"),
"HostLoggerContract"
);
assert_eq!(
host_contract_name_to_cs_caller("logger"),
"HostLoggerContract"
);
}
#[test]
fn cs_guest_caller_param_type_name_mappings() {
assert_eq!(
cs_guest_caller_param_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint"
);
assert_eq!(
cs_guest_caller_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"string"
);
assert_eq!(
cs_guest_caller_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"byte[]"
);
assert_eq!(
cs_guest_caller_param_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"ref MyStruct"
);
}
#[test]
fn cs_guest_caller_return_type_name_mappings() {
assert_eq!(
cs_guest_caller_return_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint"
);
assert_eq!(
cs_guest_caller_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"System.ReadOnlySpan<byte>"
);
assert_eq!(
cs_guest_caller_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"System.ReadOnlySpan<byte>"
);
assert_eq!(
cs_guest_caller_return_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"MyStruct"
);
}
#[test]
fn generate_cs_guest_host_contract_caller_produces_class() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
};
let mut out: String = String::new();
generate_cs_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains("public sealed class HostLoggerContract"),
"missing class: {out}"
);
assert!(
out.contains("private readonly IntPtr _instance"),
"missing instance field: {out}"
);
assert!(
out.contains("private readonly IntPtr _interface"),
"missing interface field: {out}"
);
assert!(
out.contains("public static HostLoggerContract? FromHost"),
"missing FromHost method: {out}"
);
assert!(
out.contains("public void Log(string message)"),
"missing Log method: {out}"
);
assert!(
out.contains("public bool IsValid => _interface != IntPtr.Zero"),
"missing IsValid property: {out}"
);
assert!(
out.contains("HostContractInterface*"),
"must use HostContractInterface*, not a phantom type: {out}"
);
assert!(
out.contains("ResolveHostContractInterface"),
"must resolve interface via ResolveHostContractInterface: {out}"
);
assert!(
out.contains("GetHostContract"),
"must get instance via GetHostContract: {out}"
);
assert!(
out.contains("contract->DispatchType"),
"must read DispatchType from contract: {out}"
);
assert!(
!out.contains("HostContractVTable"),
"must not reference nonexistent HostContractVTable: {out}"
);
assert!(
!out.contains("header->Header"),
"must not reference phantom header->Header: {out}"
);
assert!(
!out.contains("header->Dispatch.Native.ImplPtr"),
"must not reference phantom ImplPtr: {out}"
);
assert!(
!out.contains("header->Dispatch.VM.BridgeData"),
"must not reference phantom BridgeData: {out}"
);
assert!(
!out.contains("header->Dispatch.VM.Call("),
"must not call dispatch via phantom VM.Call: {out}"
);
}
#[test]
fn generate_cs_guest_host_contracts_file_produces_file() {
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![],
returns: None,
}],
}],
bundle: None,
};
let out: String = generate_cs_guest_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("public sealed class HostLoggerContract"),
"missing class: {out}"
);
assert!(
out.contains("HOSTLOGGERCONTRACT_ID"),
"missing constant: {out}"
);
assert_eq!(
out.matches("internal static class HostCallerFailureLog")
.count(),
1,
"HostCallerFailureLog helper must be emitted exactly once: {out}"
);
}
#[test]
fn generate_cs_guest_host_contract_method_logs_failures() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: Some(ResolvedTypeRef::Primitive(PrimitiveType::U32)),
}],
};
let mut out: String = String::new();
generate_cs_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains(
"HostCallerFailureLog.Log(_host, \"HostLoggerContract.Log\", (uint)AbiErrorCode.InvalidPointer);"
),
"null-interface path must log InvalidPointer: {out}"
);
assert!(
out.contains(
"HostCallerFailureLog.Log(_host, \"HostLoggerContract.Log\", (uint)AbiErrorCode.FunctionNotAvailable);"
),
"bounds-check path must log FunctionNotAvailable: {out}"
);
assert!(
out.contains("HostCallerFailureLog.Log(_host, \"HostLoggerContract.Log\", err.Code);"),
"ABI error path must log err.Code: {out}"
);
let lines: Vec<&str> = out.lines().collect();
for (i, line) in lines.iter().enumerate() {
if line.contains("return default(") {
assert!(
i > 0 && lines[i - 1].contains("HostCallerFailureLog.Log("),
"silent default return at line {i}: {line}\n{out}"
);
}
}
}
#[test]
fn generate_guest_with_host_contracts_produces_guest_host_contracts_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![],
returns: None,
}],
}],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
names.contains(&"guest/HostContracts.cs".to_owned()),
"missing guest/HostContracts.cs: {names:?}"
);
}
#[test]
fn generate_guest_without_host_contracts_no_guest_host_contracts_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
!names.contains(&"guest/HostContracts.cs".to_owned()),
"unexpected guest/HostContracts.cs: {names:?}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_interface_factories_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![],
returns: None,
}],
}],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
names.contains(&"host/InterfaceFactories.cs".to_owned()),
"missing host/InterfaceFactories.cs: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_interface_factories_file() {
let generator: CSharpGenerator = CSharpGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
!names.contains(&"host/InterfaceFactories.cs".to_owned()),
"unexpected host/InterfaceFactories.cs: {names:?}"
);
}
#[test]
fn generate_cs_host_interface_factories_file_produces_file() {
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
}],
bundle: None,
};
let out: String = generate_cs_host_interface_factories_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("CreateHostLoggerInterface<T>"),
"missing NATIVE factory: {out}"
);
assert!(
out.contains("CreateHostLoggerInterfaceVm"),
"missing VM factory: {out}"
);
assert!(
out.contains("where T: IHostLogger"),
"missing interface constraint: {out}"
);
assert!(
out.contains("HostContractInterface"),
"factories must return the canonical HostContractInterface: {out}"
);
assert!(
out.contains("new VmDispatch {"),
"VM factory must build the canonical VmDispatch: {out}"
);
assert!(
!out.contains("HostContractVTable"),
"must not reference the nonexistent HostContractVTable type: {out}"
);
}
#[test]
fn generate_cs_host_interface_factory_produces_native_and_vm_factories() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
};
let mut out: String = String::new();
generate_cs_host_interface_factory(&mut out, &contract, &[]);
assert!(
out.contains("CreateHostLoggerInterface<T>"),
"missing NATIVE factory: {out}"
);
assert!(
out.contains("CreateHostLoggerInterfaceVm"),
"missing VM factory: {out}"
);
assert!(
out.contains("DispatchType.Native"),
"missing Native dispatch type: {out}"
);
assert!(
out.contains("DispatchType.VirtualMachine"),
"missing VM dispatch type: {out}"
);
assert!(
out.contains("host_logger_log_thunk"),
"missing thunk function: {out}"
);
assert!(
!out.contains("private static IHostLogger"),
"Rule 12 violation: impl must not live in a class static: {out}"
);
assert!(
!out.contains("private static GCHandle"),
"Rule 12 violation: pinned functions handle must not be a class static: {out}"
);
assert!(
out.contains("private sealed class HostLoggerHostState"),
"missing per-registration state class: {out}"
);
assert!(
out.contains("var stateHandle = GCHandle.Alloc(state);"),
"state must be rooted by a GCHandle: {out}"
);
assert!(
out.contains("UserData = GCHandle.ToIntPtr(stateHandle),"),
"UserData must carry the per-registration state token: {out}"
);
assert!(
out.contains("((HostContractInterface*)self)->UserData"),
"create stub must copy the interface UserData into instance Data: {out}"
);
assert!(
out.contains("(HostLoggerHostState)GCHandle.FromIntPtr(implPtr).Target!"),
"thunk must recover the impl from the instance token, not a static: {out}"
);
}
#[test]
fn peer_caller_emitted_for_declared_dependency() {
let generator: CSharpGenerator = CSharpGenerator;
let validator_contract_id: u64 = polyplug_utils::guest_contract_id("pipeline.Validator", 1);
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.Validator".to_owned(),
contract_id: validator_contract_id,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "validate".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "input".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
}],
host_contracts: vec![],
bundle: Some(ResolvedBundle {
name: "csharp_transformer".to_owned(),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
loader: "dotnet".to_owned(),
file: polyplug_codegen::ResolvedBundleFile::Single("transformer.dll".to_owned()),
plugins: vec![],
bundle_id: 0xDEAD_BEEF_CAFE_0001_u64,
dependencies: vec![ResolvedDependency::ByContract {
contract: "pipeline.Validator".to_owned(),
contract_id: validator_contract_id,
min_version: 1,
}],
needs_reinit_on_dep_reload: false,
}),
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
names.contains(&"guest/PeerCallers.cs".to_owned()),
"missing guest/PeerCallers.cs: {names:?}"
);
let content: &str = files
.files
.iter()
.find(|f: &&GeneratedFile| f.path.to_string_lossy() == "guest/PeerCallers.cs")
.expect("PeerCallers.cs")
.content
.as_str();
assert!(
content.contains("PipelineValidatorContractPeer"),
"missing peer class: {content}"
);
assert!(
content.contains("switch (_interface->DispatchType)"),
"peer dispatch must branch on the cached interface's DispatchType: {content}"
);
assert!(
content.contains("_interface->Dispatch.Native.Functions"),
"peer Native arm must call through the native function table: {content}"
);
assert!(
content.contains("_interface->Dispatch.Vm.Call"),
"peer VM arm must call through the vm.call trampoline: {content}"
);
assert!(
content.contains("Resolve(IntPtr hostPtr)"),
"peer class must take the host pointer explicitly in Resolve: {content}"
);
assert!(
!content.contains("RuntimeAbiStorage"),
"peer class must not read a stored host pointer: {content}"
);
}
#[test]
fn no_peer_callers_without_dependencies() {
let generator: CSharpGenerator = CSharpGenerator;
let ir_no_bundle: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.Validator".to_owned(),
contract_id: polyplug_utils::guest_contract_id("pipeline.Validator", 1),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir_no_bundle, &mut files)
.expect("generate_guest (no bundle)");
let names: Vec<String> = files
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
!names.contains(&"guest/PeerCallers.cs".to_owned()),
"guest/PeerCallers.cs must NOT be emitted when bundle is None: {names:?}"
);
let ir_no_deps: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.Encoder".to_owned(),
contract_id: polyplug_utils::guest_contract_id("pipeline.Encoder", 1),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
bundle: Some(ResolvedBundle {
name: "csharp_encoder".to_owned(),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
loader: "dotnet".to_owned(),
file: polyplug_codegen::ResolvedBundleFile::Single("encoder.dll".to_owned()),
plugins: vec![],
bundle_id: 0xAAAA_BBBB_0000_0001_u64,
dependencies: vec![],
needs_reinit_on_dep_reload: false,
}),
};
let mut files2: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir_no_deps, &mut files2)
.expect("generate_guest (no deps)");
let names2: Vec<String> = files2
.files
.iter()
.map(|f: &GeneratedFile| f.path.to_string_lossy().to_string())
.collect();
assert!(
!names2.contains(&"guest/PeerCallers.cs".to_owned()),
"guest/PeerCallers.cs must NOT be emitted when no deps: {names2:?}"
);
}
#[test]
fn cs_host_thunk_single_enum_param_reads_repr_integer() {
let enums: Vec<EnumDef> = vec![EnumDef {
name: "LogLevel".to_owned(),
repr: ReprType::U32,
bitflag: false,
variants: vec![EnumVariant {
name: "Info".to_owned(),
value: "1".to_owned(),
}],
}];
let func: ResolvedFunction = ResolvedFunction {
name: "set_level".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "level".to_owned(),
ty: ResolvedTypeRef::UserDefined("LogLevel".to_owned()),
}],
returns: None,
};
let mut out: String = String::new();
generate_cs_host_thunk_args(&mut out, &func, "UnusedArgs", &enums);
assert!(
out.contains("var level = (LogLevel)Marshal.PtrToStructure<uint>(argsPtr);"),
"single enum param must read the repr integer and cast: {out}"
);
assert!(
!out.contains("PtrToStructure<LogLevel>"),
"PtrToStructure must never be instantiated with an enum type: {out}"
);
}
#[test]
fn cs_host_thunk_single_struct_param_keeps_ptr_to_structure() {
let func: ResolvedFunction = ResolvedFunction {
name: "describe".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "desc".to_owned(),
ty: ResolvedTypeRef::UserDefined("ImageDesc".to_owned()),
}],
returns: None,
};
let mut out: String = String::new();
generate_cs_host_thunk_args(&mut out, &func, "UnusedArgs", &[]);
assert!(
out.contains("var desc = Marshal.PtrToStructure<ImageDesc>(argsPtr);"),
"struct params marshal directly: {out}"
);
}
}