use super::CALL_ARENA_BUF_LEN;
use super::CodeGenerator;
use super::GeneratedFile;
use super::GeneratedFiles;
use super::collect_peer_contracts;
use super::is_native_runtime;
use super::peer_min_version;
use crate::ir::AbiBuiltin;
use crate::ir::EnumDef;
use crate::ir::EnumVariant;
use crate::ir::PrimitiveType;
use crate::ir::ResolvedBundle;
use crate::ir::ResolvedContract;
use crate::ir::ResolvedDependency;
use crate::ir::ResolvedFunction;
use crate::ir::ResolvedHostContract;
use crate::ir::ResolvedParam;
use crate::ir::ResolvedPlugin;
use crate::ir::ResolvedType;
use crate::ir::ResolvedTypeRef;
use crate::ir::ValidatedIr;
use polyplug_codegen::PolyplugcError;
pub(crate) struct RustGenerator;
const RUST_FILE_HEADER: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n";
impl CodeGenerator for RustGenerator {
fn generate_host(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --api api.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut types_out: String = String::new();
types_out.push_str(header);
types_out.push_str("use polyplug_abi::StringView;\n");
types_out.push_str("use polyplug_abi::Version;\n\n");
for e in &ir.enums {
generate_rust_enum(&mut types_out, e);
}
for ty in &ir.types {
generate_rust_type(&mut types_out, ty);
}
for contract in &ir.contracts {
let struct_name: String = contract_name_to_struct(&contract.name);
for func in &contract.functions {
if needs_arg_pack(&func.params) {
emit_arg_pack_struct(&mut types_out, &struct_name, func);
}
}
}
for contract in &ir.host_contracts {
let struct_name: String = host_contract_name_to_trait(&contract.name);
for func in &contract.functions {
if needs_arg_pack(&func.params) {
emit_arg_pack_struct(&mut types_out, &struct_name, func);
}
}
}
if let Some(ref bundle) = ir.bundle {
types_out.push_str(&format!(
"pub const MY_BUNDLE_ID: u64 = {};\n\n",
bundle.bundle_id
));
for dep in &bundle.dependencies {
let contract_upper: String = dep_contract_name(dep)
.to_uppercase()
.replace(['.', '-'], "_");
types_out.push_str(&format!(
"pub const DEP_{contract_upper}: u64 = {};\n",
dep_contract_id(dep)
));
types_out.push_str(&format!(
"pub const DEP_{contract_upper}_MIN_VERSION: u32 = {};\n",
dep_min_version(dep)
));
}
types_out.push('\n');
}
for contract in &ir.contracts {
let contract_upper: String = contract.name.to_uppercase().replace(['.', '-'], "_");
types_out.push_str(&format!(
"pub const {contract_upper}_CONTRACT_ID: u64 = {:#018x};\n",
contract.contract_id
));
types_out.push_str(&format!(
"pub const {contract_upper}_REQUIRED_VERSION: Version = \
Version {{ major: {major}, minor: {minor}, patch: 0 }};\n",
major = contract.version.major,
minor = contract.version.minor,
));
types_out.push_str(&format!(
"pub const {contract_upper}_REQUIRED_FUNCTION_COUNT: u32 = {};\n",
contract.functions.len()
));
}
types_out.push('\n');
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/types.rs"),
content: types_out,
force_regenerate: false,
});
let mut callers_out: String = String::new();
callers_out.push_str(header);
callers_out.push_str("use polyplug_abi::AbiErrorCode;\n");
callers_out.push_str("use polyplug_abi::AbiError;\n");
callers_out.push_str("use polyplug_abi::GuestContractInterface;\n");
callers_out.push_str("use polyplug_abi::HostApi;\n");
callers_out.push_str("use polyplug_abi::DispatchType;\n");
callers_out.push_str("use polyplug_abi::StringView;\n");
callers_out.push_str("use polyplug_abi::GuestContractHandle;\n");
callers_out.push_str("use polyplug_abi::GuestContractInstance;\n");
callers_out.push_str("use polyplug_abi::CallArena;\n");
callers_out.push_str("use super::types::*;\n\n");
callers_out.push_str("/// Size of each caller's inline call-arena buffer.\n");
callers_out.push_str("///\n");
callers_out
.push_str("/// Variable-size VM return values (strings, buffers) are bump-allocated\n");
callers_out
.push_str("/// from this buffer; outputs larger than it spill into host-allocated\n");
callers_out.push_str("/// overflow blocks that the arena frees on the next reset.\n");
callers_out.push_str(&format!(
"const CALL_ARENA_BUF_LEN: usize = {CALL_ARENA_BUF_LEN};\n\n"
));
callers_out.push_str("/// Host-side error type for contract calls.\n");
callers_out.push_str("#[derive(Debug)]\n");
callers_out.push_str("pub struct ContractError {\n");
callers_out.push_str(" /// ABI error code (non-zero).\n");
callers_out.push_str(" pub code: AbiErrorCode,\n");
callers_out.push_str(" /// Human-readable error message (may be empty).\n");
callers_out.push_str(" pub message: String,\n");
callers_out.push_str("}\n\n");
callers_out.push_str("impl ContractError {\n");
callers_out.push_str(" /// Create a new error with the given code.\n");
callers_out.push_str(" pub fn new(code: AbiErrorCode) -> Self {\n");
callers_out.push_str(" Self { code, message: String::new() }\n");
callers_out.push_str(" }\n");
callers_out.push_str("}\n\n");
for contract in &ir.contracts {
generate_host_contract_caller(&mut callers_out, contract)?;
}
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/host_callers.rs"),
content: callers_out,
force_regenerate: false,
});
if !ir.host_contracts.is_empty() {
let host_contracts_out: String = generate_host_contracts_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/host_contracts.rs"),
content: host_contracts_out,
force_regenerate: false,
});
}
if !ir.host_contracts.is_empty() {
let interface_factories_out: String = generate_host_interface_factories_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/interface_factories.rs"),
content: interface_factories_out,
force_regenerate: false,
});
}
let mut host_mod_content: String = String::new();
host_mod_content.push_str(RUST_FILE_HEADER);
host_mod_content.push_str("pub mod types;\n");
host_mod_content.push_str("pub mod host_callers;\n");
if !ir.host_contracts.is_empty() {
host_mod_content.push_str("pub mod host_contracts;\n");
host_mod_content.push_str("pub mod interface_factories;\n");
}
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/mod.rs"),
content: host_mod_content,
force_regenerate: true,
});
let mut root_mod_content: String = String::new();
root_mod_content.push_str(RUST_FILE_HEADER);
root_mod_content.push_str("pub mod host;\n");
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("mod.rs"),
content: root_mod_content,
force_regenerate: true,
});
let manifest_content: String = [
"# THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n",
"[manifest]\n",
"schema_version = 1\n",
"lang = \"rust\"\n",
"generated_by = \"polyplugc\"\n",
]
.concat();
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("manifest.toml"),
content: manifest_content,
force_regenerate: true,
});
Ok(())
}
fn generate_guest(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --api api.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut types_out: String = String::new();
types_out.push_str(header);
types_out.push_str("use polyplug_abi::StringView;\n\n");
for e in &ir.enums {
generate_rust_enum(&mut types_out, e);
}
for ty in &ir.types {
generate_rust_type(&mut types_out, ty);
}
for contract in &ir.contracts {
let struct_name: String = contract_name_to_struct(&contract.name);
for func in &contract.functions {
if needs_arg_pack(&func.params) {
emit_arg_pack_struct(&mut types_out, &struct_name, func);
}
}
}
for contract in &ir.host_contracts {
let struct_name: String = host_contract_name_to_trait(&contract.name);
for func in &contract.functions {
if needs_arg_pack(&func.params) {
emit_arg_pack_struct(&mut types_out, &struct_name, func);
}
}
}
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/types.rs"),
content: types_out,
force_regenerate: false,
});
let mut contracts_out: String = String::new();
contracts_out.push_str(header);
contracts_out.push_str("use polyplug_abi::StringView;\n");
contracts_out.push_str("use polyplug_guest::GuestError;\n");
contracts_out.push_str("use super::types::*;\n\n");
for contract in &ir.contracts {
generate_guest_contract_trait(&mut contracts_out, contract);
}
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/contracts.rs"),
content: contracts_out,
force_regenerate: false,
});
let mut interfaces_out: String = String::new();
interfaces_out.push_str(header);
generate_guest_interfaces_file(&mut interfaces_out, ir)?;
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/interfaces.rs"),
content: interfaces_out,
force_regenerate: false,
});
let mut init_out: String = String::new();
init_out.push_str(header);
generate_guest_init_file(&mut init_out, ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/init.rs"),
content: init_out,
force_regenerate: false,
});
if !ir.host_contracts.is_empty() {
let host_contract_callers_out: String = generate_guest_host_contracts_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/host_contract_callers.rs"),
content: host_contract_callers_out,
force_regenerate: false,
});
}
let peer_contracts: Vec<&ResolvedContract> = collect_peer_contracts(ir);
if !peer_contracts.is_empty() {
let peer_callers_out: String = generate_peer_callers_file(ir, &peer_contracts);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/peer_callers.rs"),
content: peer_callers_out,
force_regenerate: false,
});
}
let guest_mod_content: String = generate_guest_mod_rs(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/mod.rs"),
content: guest_mod_content,
force_regenerate: true,
});
if ir.bundle.is_some() {
let manifest_content: String = generate_bundle_manifest(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("manifest.toml"),
content: manifest_content,
force_regenerate: true,
});
}
Ok(())
}
}
fn generate_bundle_manifest(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 file_field: String = super::format_manifest_file_field(&bundle.file);
let mut provides: Vec<String> = bundle
.plugins
.iter()
.flat_map(|p: &ResolvedPlugin| 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()
.map(|s: &String| {
if let Some(at_pos) = s.find('@') {
let contract_name: &str = &s[..at_pos];
let version_part: &str = &s[at_pos + 1..];
if let Some(dot_pos) = version_part.find('.') {
let major: &str = &version_part[..dot_pos];
format!("{}@{}", contract_name, major)
} else {
s.clone()
}
} else {
s.clone()
}
})
.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 dep_toml: String = super::emit_manifest_dependencies(&bundle.dependencies);
let reinit: bool = bundle.needs_reinit_on_dep_reload;
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_toml}",
bundle_id = bundle.bundle_id
)
}
fn generate_guest_contract_trait(out: &mut String, contract: &ResolvedContract) {
let trait_name: String = contract_name_to_guest_trait(&contract.name);
out.push_str(&format!(
"/// Guest trait for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str(&format!("pub trait {trait_name}: Send + Sync {{\n"));
for func in &contract.functions {
generate_guest_trait_method(out, func, &contract_name_to_struct(&contract.name));
}
out.push_str("}\n\n");
}
fn generate_guest_trait_method(out: &mut String, func: &ResolvedFunction, contract_struct: &str) {
let ret_type: String = match &func.returns {
Some(ty) => rust_type_name(ty),
None => "()".to_owned(),
};
let sig_params: String = build_guest_trait_params(func, contract_struct);
out.push_str(&format!(
" fn {}(&self{}) -> Result<{ret_type}, GuestError>;\n",
func.name, sig_params
));
}
fn build_guest_trait_params(func: &ResolvedFunction, contract_struct: &str) -> String {
if func.params.is_empty() {
return String::new();
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
return match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => {
format!(", {}: &{}", param.name, rust_type_name(¶m.ty))
}
_ => format!(", {}: {}", param.name, rust_type_name(¶m.ty)),
};
}
let _ = contract_struct;
func.params
.iter()
.map(|p: &ResolvedParam| format!(", {}: {}", p.name, rust_type_name(&p.ty)))
.collect::<Vec<_>>()
.join("")
}
fn contract_name_to_guest_trait(name: &str) -> String {
name.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("")
+ "GuestContract"
}
fn contract_name_to_upper_snake(name: &str) -> String {
name.replace('.', "_").to_uppercase()
}
fn generate_guest_interfaces_file(
out: &mut String,
ir: &ValidatedIr,
) -> Result<(), PolyplugcError> {
out.push_str("use core::ffi::c_void;\n");
out.push_str("use polyplug_abi::AbiError;\n");
out.push_str("use polyplug_abi::AbiErrorCode;\n");
out.push_str("use polyplug_abi::GuestContractInterface;\n");
out.push_str("use polyplug_abi::GuestContractInstance;\n");
out.push_str("use polyplug_abi::HostApi;\n");
out.push_str("use polyplug_abi::DispatchType;\n");
out.push_str("use polyplug_abi::NativeDispatch;\n");
out.push_str("use polyplug_abi::DispatchMechanisms;\n");
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::Version;\n");
out.push_str("use polyplug_abi::string_view_null;\n");
out.push_str("use polyplug_abi::string_view_from_static;\n");
out.push_str("use polyplug_abi::abi_error_ok;\n");
out.push_str("use polyplug_guest::GuestError;\n");
out.push_str("use polyplug_guest::HostContext;\n");
out.push_str("use polyplug_utils::GuestContractId;\n");
out.push_str("use super::types::*;\n");
out.push_str(
"/// Convert a GuestError to an AbiError, allocating the message via the host allocator.\n",
);
out.push_str("/// Falls back to a null message if allocation fails.\n");
out.push_str("fn plugin_error_to_abi_error(host: HostContext, e: GuestError) -> AbiError {\n");
out.push_str(" let message: StringView = host.alloc_string(&e.message).unwrap_or_else(|_| string_view_null());\n");
out.push_str(" AbiError { code: e.code as u32, message }\n");
out.push_str("}\n\n");
for contract in &ir.contracts {
let trait_name: String = contract_name_to_guest_trait(&contract.name);
out.push_str(&format!("use super::contracts::{trait_name};\n"));
}
out.push_str("/// Wrapper for a function pointer stored in a static interface array.\n");
out.push_str("#[repr(transparent)]\n");
out.push_str("pub struct FnPtr(pub *const ());\n");
out.push_str("// SAFETY: FnPtr wraps a 'static function pointer. Function pointers are safe\n");
out.push_str(
"// to share across threads — the function itself handles its own synchronization.\n",
);
out.push_str("unsafe impl Send for FnPtr {}\n");
out.push_str(
"// SAFETY: Function pointers are inherently Sync — multiple threads may call the\n",
);
out.push_str(
"// same function concurrently. The underlying data is read-only 'static memory.\n",
);
out.push_str("unsafe impl Sync for FnPtr {}\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_guest_plugin_interface(
out,
&plugin.name,
contract,
is_native_runtime(&bundle.loader),
)?;
}
}
}
} else {
for contract in &ir.contracts {
generate_guest_contract_interface(out, contract, true)?;
}
}
Ok(())
}
fn generate_guest_contract_interface(
out: &mut String,
contract: &ResolvedContract,
is_native: bool,
) -> Result<(), PolyplugcError> {
let upper: String = contract_name_to_upper_snake(&contract.name);
let struct_name: String = contract_name_to_struct(&contract.name);
let trait_name: String = contract_name_to_guest_trait(&contract.name);
let lower: String = contract.name.replace('.', "_");
let fn_count: usize = contract.functions.len();
out.push_str(&format!(
"/// Contract ID constant -- pre-computed FNV-1a of \"{}@{}\".\n",
contract.name, contract.version.major
));
out.push_str(&format!(
"pub(crate) const {upper}_CONTRACT_ID: u64 = 0x{:016X};\n\n",
contract.contract_id
));
let state_struct: String = format!("{struct_name}PluginState");
emit_guest_instance_machinery(
out,
&upper,
&lower,
&trait_name,
&state_struct,
&format!("{upper}_CONTRACT_ID"),
);
for func in &contract.functions {
generate_guest_abi_wrapper(
out,
func,
&lower,
&state_struct,
&struct_name,
&trait_name,
contract,
)?;
}
out.push_str(&format!(
"static {upper}_FNS: [FnPtr; {fn_count}_usize] = [\n"
));
for func in &contract.functions {
out.push_str(&format!(
" FnPtr({lower}_{}_abi as *const ()),\n",
func.name
));
}
out.push_str("];\n\n");
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(
"pub(crate) static {upper}_INTERFACE: GuestContractInterface = GuestContractInterface {{\n"
));
out.push_str(&format!(
" contract_id: GuestContractId::from_u64({upper}_CONTRACT_ID),\n"
));
out.push_str(&format!(
" contract_version: Version {{ major: {major}, minor: {minor}, patch: {patch} }},\n"
));
let dispatch_type_str: &str = if is_native {
"DispatchType::Native"
} else {
"DispatchType::VirtualMachine"
};
out.push_str(&format!(" dispatch_type: {dispatch_type_str},\n"));
out.push_str(&format!(" create_instance: {upper}_create_instance,\n"));
out.push_str(&format!(
" destroy_instance: {upper}_destroy_instance,\n"
));
out.push_str(" dispatch: DispatchMechanisms {\n");
out.push_str(" native: NativeDispatch {\n");
out.push_str(&format!(" function_count: {fn_count}_u32,\n"));
out.push_str(&format!(
" functions: {upper}_FNS.as_ptr() as *const *const (),\n"
));
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str("};\n\n");
Ok(())
}
fn generate_guest_plugin_interface(
out: &mut String,
plugin_name: &str,
contract: &ResolvedContract,
is_native: bool,
) -> Result<(), PolyplugcError> {
let plugin_upper: String = plugin_name.to_uppercase().replace('.', "_");
let plugin_lower: String = plugin_name.to_lowercase().replace('.', "_");
let struct_name: String = contract_name_to_struct(&contract.name);
let trait_name: String = contract_name_to_guest_trait(&contract.name);
let fn_count: usize = contract.functions.len();
out.push_str(&format!("/// Plugin: {plugin_name}\n"));
out.push_str(&format!(
"/// Contract ID constant -- pre-computed FNV-1a of \"{}@{}\".\n",
contract.name, contract.version.major
));
out.push_str(&format!(
"pub const {plugin_upper}_CONTRACT_ID: u64 = 0x{:016X};\n\n",
contract.contract_id
));
let state_struct: String = format!("{}PluginState", to_pascal_ident(&plugin_lower));
emit_guest_instance_machinery(
out,
&plugin_upper,
&plugin_lower,
&trait_name,
&state_struct,
&format!("{plugin_upper}_CONTRACT_ID"),
);
for func in &contract.functions {
generate_guest_abi_wrapper(
out,
func,
&plugin_lower,
&state_struct,
&struct_name,
&trait_name,
contract,
)?;
}
out.push_str(&format!(
"static {plugin_upper}_FNS: [FnPtr; {fn_count}_usize] = [\n"
));
for func in &contract.functions {
out.push_str(&format!(
" FnPtr({plugin_lower}_{}_abi as *const ()),\n",
func.name
));
}
out.push_str("];\n\n");
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(
"pub static {plugin_upper}_INTERFACE: GuestContractInterface = GuestContractInterface {{\n"
));
out.push_str(&format!(
" contract_id: GuestContractId::from_u64({plugin_upper}_CONTRACT_ID),\n"
));
out.push_str(&format!(
" contract_version: Version {{ major: {major}, minor: {minor}, patch: {patch} }},\n"
));
let dispatch_type_str: &str = if is_native {
"DispatchType::Native"
} else {
"DispatchType::VirtualMachine"
};
out.push_str(&format!(" dispatch_type: {dispatch_type_str},\n"));
out.push_str(&format!(
" create_instance: {plugin_upper}_create_instance,\n"
));
out.push_str(&format!(
" destroy_instance: {plugin_upper}_destroy_instance,\n"
));
out.push_str(" dispatch: DispatchMechanisms {\n");
out.push_str(" native: NativeDispatch {\n");
out.push_str(&format!(" function_count: {fn_count}_u32,\n"));
out.push_str(&format!(
" functions: {plugin_upper}_FNS.as_ptr() as *const *const (),\n"
));
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str("};\n\n");
Ok(())
}
fn emit_guest_instance_machinery(
out: &mut String,
prefix_upper: &str,
lower: &str,
trait_name: &str,
state_struct: &str,
contract_id_const: &str,
) {
let factory_name: String = format!("polyplug_create_{lower}");
out.push_str("unsafe extern \"Rust\" {\n");
out.push_str(&format!(
" /// Author-provided factory — define it in the plugin crate as:\n\
\x20 /// `#[unsafe(no_mangle)]`\n\
\x20 /// `pub fn {factory_name}(host: HostContext) -> Box<dyn {trait_name}> {{ ... }}`\n"
));
out.push_str(&format!(
" fn {factory_name}(host: HostContext) -> Box<dyn {trait_name}>;\n"
));
out.push_str("}\n\n");
out.push_str(&format!(
"/// Per-instance payload carried in `GuestContractInstance.data`.\n\
struct {state_struct} {{\n\
\x20 /// Host context captured at instance creation — routes every host call\n\
\x20 /// (allocation, logging, error reporting) to the runtime that owns it.\n\
\x20 host: HostContext,\n\
\x20 /// The author's implementation, created by `{factory_name}`.\n\
\x20 implementation: Box<dyn {trait_name}>,\n\
}}\n\n"
));
out.push_str(&format!(
"/// Create a new instance: calls the author factory and boxes the payload.\n\
/// Writes a null handle through `out_instance` when `host` is null or the factory panics.\n\
unsafe extern \"C\" fn {prefix_upper}_create_instance(\n\
\x20 _loader_data: polyplug_abi::dispatch::VmLoaderData,\n\
\x20 host: *const HostApi,\n\
\x20 _args: *const (),\n\
\x20 out_instance: *mut GuestContractInstance,\n\
) {{\n\
\x20 if out_instance.is_null() {{\n\
\x20 return;\n\
\x20 }}\n\
\x20 if host.is_null() {{\n\
\x20 // SAFETY: out_instance is non-null (checked above) and writable per the ABI contract.\n\
\x20 unsafe {{ out_instance.write(GuestContractInstance::null()); }}\n\
\x20 return;\n\
\x20 }}\n\
\x20 // SAFETY: host is non-null (checked above) and valid per the ABI contract\n\
\x20 // for create_instance; it stays valid for the runtime's lifetime.\n\
\x20 let host_ctx: HostContext = unsafe {{ HostContext::new(host) }};\n\
\x20 let implementation: Box<dyn {trait_name}> =\n\
\x20 match std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {{\n\
\x20 // SAFETY: the author defines `{factory_name}` in the plugin crate\n\
\x20 // with `#[unsafe(no_mangle)]`; same-crate Rust ABI, signature enforced\n\
\x20 // by the extern declaration above.\n\
\x20 unsafe {{ {factory_name}(host_ctx) }}\n\
\x20 }})) {{\n\
\x20 Ok(i) => i,\n\
\x20 Err(_) => {{\n\
\x20 // SAFETY: out_instance is non-null and writable per the ABI contract.\n\
\x20 unsafe {{ out_instance.write(GuestContractInstance::null()); }}\n\
\x20 return;\n\
\x20 }}\n\
\x20 }};\n\
\x20 let state: Box<{state_struct}> = Box::new({state_struct} {{\n\
\x20 host: host_ctx,\n\
\x20 implementation,\n\
\x20 }});\n\
\x20 // SAFETY: out_instance is non-null (checked above) and writable per the ABI contract.\n\
\x20 unsafe {{\n\
\x20 out_instance.write(GuestContractInstance {{\n\
\x20 data: Box::into_raw(state) as *mut c_void,\n\
\x20 contract_id: GuestContractId::from_u64({contract_id_const}),\n\
\x20 }});\n\
\x20 }}\n\
}}\n\n"
));
out.push_str(&format!(
"/// Destroy an instance created by `{prefix_upper}_create_instance`.\n\
unsafe extern \"C\" fn {prefix_upper}_destroy_instance(\n\
\x20 _loader_data: polyplug_abi::dispatch::VmLoaderData,\n\
\x20 _host: *const HostApi,\n\
\x20 instance: GuestContractInstance,\n\
) {{\n\
\x20 if instance.data.is_null() {{\n\
\x20 return;\n\
\x20 }}\n\
\x20 // SAFETY: instance.data was produced by `{prefix_upper}_create_instance` via\n\
\x20 // Box::into_raw and is dropped exactly once here — the host calls\n\
\x20 // destroy_instance exactly once per created instance.\n\
\x20 drop(unsafe {{ Box::from_raw(instance.data as *mut {state_struct}) }});\n\
}}\n\n"
));
}
fn to_pascal_ident(s: &str) -> String {
s.split('_')
.map(|seg: &str| {
let mut chars: core::str::Chars<'_> = seg.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("")
}
fn generate_guest_abi_wrapper(
out: &mut String,
func: &ResolvedFunction,
contract_lower: &str,
state_struct: &str,
contract_struct: &str,
trait_name: &str,
_contract: &ResolvedContract,
) -> Result<(), PolyplugcError> {
let wrapper_name: String = format!("{contract_lower}_{}_abi", func.name);
let has_return: bool = func.returns.is_some();
let out_param: &str = if has_return {
"out: *mut ()"
} else {
"_out: *mut ()"
};
out.push_str(&format!(
"/// ABI wrapper for {} (function_id = {}).\n",
func.name, func.function_id
));
out.push_str("// SAFETY: args and out pointers are validated at entry before dereferencing.\n");
out.push_str("#[allow(clippy::unnecessary_cast)]\n");
out.push_str(&format!(
"extern \"C\" fn {wrapper_name}(instance: GuestContractInstance, args: *const (), {out_param}, out_err: *mut AbiError) {{\n"
));
out.push_str(" let __result_err: AbiError = (|| {\n");
out.push_str(" if instance.data.is_null() {\n");
out.push_str(" return AbiError { code: AbiErrorCode::InvalidPointer as u32, message: string_view_from_static(b\"instance is null\") };\n");
out.push_str(" }\n");
out.push_str(&format!(
" // SAFETY: instance.data was produced by create_instance via Box::into_raw\n\
\x20 // and stays valid until destroy_instance; the host never mutates it.\n\
\x20 let state: &{state_struct} = unsafe {{ &*(instance.data as *const {state_struct}) }};\n"
));
out.push_str(" match std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {\n");
out.push_str(&format!(
" let impl_ref: &dyn {trait_name} = state.implementation.as_ref();\n"
));
if !func.params.is_empty() {
out.push_str(" if args.is_null() {\n");
out.push_str(" return AbiError { code: AbiErrorCode::InvalidPointer as u32, message: string_view_from_static(b\"args pointer is null\") };\n");
out.push_str(" }\n");
}
if has_return {
out.push_str(" if out.is_null() {\n");
out.push_str(" return AbiError { code: AbiErrorCode::InvalidPointer as u32, message: string_view_from_static(b\"out pointer is null\") };\n");
out.push_str(" }\n");
}
emit_guest_wrapper_call(out, func, contract_struct);
if has_return {
let ret_ty: String = match &func.returns {
Some(ty) => rust_type_name(ty),
None => String::new(),
};
out.push_str(" match result {\n");
out.push_str(" Ok(val) => {\n");
out.push_str(&format!(
" // SAFETY: out is a valid *mut {ret_ty} per ABI contract.\n"
));
out.push_str(&format!(
" unsafe {{ core::ptr::write(out as *mut {ret_ty}, val); }}\n"
));
out.push_str(" abi_error_ok()\n");
out.push_str(" }\n");
out.push_str(" Err(e) => plugin_error_to_abi_error(state.host, e),\n");
out.push_str(" }\n");
} else {
out.push_str(" match result {\n");
out.push_str(" Ok(()) => abi_error_ok(),\n");
out.push_str(" Err(e) => plugin_error_to_abi_error(state.host, e),\n");
out.push_str(" }\n");
}
out.push_str(" })) {\n");
out.push_str(" Ok(err) => err,\n");
out.push_str(" Err(_) => AbiError::panic_caught(),\n");
out.push_str(" }\n");
out.push_str(" })();\n");
out.push_str(" if !out_err.is_null() {\n");
out.push_str(
" // SAFETY: out_err is a valid, writable *mut AbiError per the ABI contract.\n",
);
out.push_str(" unsafe { out_err.write(__result_err); }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
Ok(())
}
fn emit_guest_wrapper_call(out: &mut String, func: &ResolvedFunction, contract_struct: &str) {
if func.params.is_empty() {
out.push_str(" // SAFETY: no args for this function.\n");
out.push_str(" let _ = args;\n");
out.push_str(&format!(" let result = impl_ref.{}();\n", func.name));
return;
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => {
let ty_name: String = rust_type_name(¶m.ty);
out.push_str(&format!(
" // SAFETY: args is a valid *const {ty_name} per ABI contract.\n"
));
out.push_str(&format!(
" let result = impl_ref.{}(unsafe {{ &*(args as *const {ty_name}) }});\n",
func.name
));
}
_ => {
let ty_name: String = rust_type_name(¶m.ty);
out.push_str(&format!(
" // SAFETY: args is a valid *const {ty_name} per ABI contract.\n"
));
out.push_str(&format!(
" let result = impl_ref.{}(unsafe {{ *(args as *const {ty_name}) }});\n",
func.name
));
}
}
return;
}
let pack_struct: String = arg_pack_struct_name(contract_struct, &func.name);
out.push_str(&format!(
" // SAFETY: args is a valid *const {pack_struct} per ABI contract.\n"
));
out.push_str(&format!(
" let packed: &{pack_struct} = unsafe {{ &*(args as *const {pack_struct}) }};\n"
));
let call_args: String = func
.params
.iter()
.map(|p: &ResolvedParam| format!("packed.{}", p.name))
.collect::<Vec<_>>()
.join(", ");
out.push_str(&format!(
" let result = impl_ref.{}({call_args});\n",
func.name
));
}
fn generate_guest_init_file(out: &mut String, ir: &ValidatedIr) {
out.push_str("use polyplug_abi::AbiError;\n");
out.push_str("use polyplug_abi::AbiErrorCode;\n");
out.push_str("use polyplug_abi::string_view_from_static;\n");
out.push_str("use polyplug_abi::abi_error_ok;\n");
out.push_str("use polyplug_abi::PluginDescriptor;\n");
out.push_str("use polyplug_abi::HostApi;\n");
out.push_str("use polyplug_abi::GuestContractInterface;\n");
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::Version;\n");
out.push_str("use polyplug_abi::BundleInitContext;\n");
out.push_str("use core::ffi::c_void;\n");
if let Some(bundle) = &ir.bundle {
for plugin in &bundle.plugins {
let plugin_upper: String = plugin.name.to_uppercase().replace('.', "_");
out.push_str(&format!(
"use super::interfaces::{plugin_upper}_CONTRACT_ID;\n"
));
out.push_str(&format!(
"use super::interfaces::{plugin_upper}_INTERFACE;\n"
));
}
} else {
for contract in &ir.contracts {
let upper: String = contract_name_to_upper_snake(&contract.name);
out.push_str(&format!("use super::interfaces::{upper}_CONTRACT_ID;\n"));
out.push_str(&format!("use super::interfaces::{upper}_INTERFACE;\n"));
}
}
out.push('\n');
out.push_str(
"// Note: polyplug_abi_version() should be exported by the plugin crate itself,\n",
);
out.push_str("// not by the generated code. Add this to your lib.rs:\n");
out.push_str("// #[unsafe(no_mangle)]\n");
out.push_str("// pub extern \"C\" fn polyplug_abi_version() -> u32 { 1 }\n\n");
out.push_str("/// Register all plugin interfaces with the host.\n");
out.push_str("///\n");
out.push_str("/// # Safety\n");
out.push_str("/// `host` and `ctx` must be valid non-null pointers provided by the host.\n");
out.push_str("#[unsafe(no_mangle)]\n");
out.push_str("pub unsafe extern \"C\" fn polyplug_init(\n");
out.push_str(" host: *const HostApi,\n");
out.push_str(" ctx: *const BundleInitContext,\n");
out.push_str(") -> AbiError {\n");
out.push_str(" if host.is_null() {\n");
out.push_str(
" return AbiError { code: AbiErrorCode::Generic as u32, message: string_view_from_static(b\"host is null\") };\n",
);
out.push_str(" }\n");
out.push_str(" if ctx.is_null() {\n");
out.push_str(
" return AbiError { code: AbiErrorCode::Generic as u32, message: string_view_from_static(b\"ctx is null\") };\n",
);
out.push_str(" }\n");
out.push_str(" // SAFETY: ctx is non-null and valid for the lifetime of this call as guaranteed by the host.\n");
out.push_str(" let ctx: &BundleInitContext = unsafe { &*ctx };\n");
out.push_str(
" let _ = ctx; // suppress unused warning if plugin_init user stub not yet updated\n",
);
out.push_str(" // SAFETY: host is non-null and valid per ABI contract.\n");
out.push_str(" let host: &HostApi = unsafe { &*host };\n\n");
out.push_str(
" // No process-wide state is stored here. The implementation is constructed\n",
);
out.push_str(" // per instance by `create_instance` (which calls the author factory\n");
out.push_str(
" // `polyplug_create_<plugin>` with a HostContext); init only registers the\n",
);
out.push_str(" // static interface tables below.\n\n");
if let Some(bundle) = &ir.bundle {
for plugin in &bundle.plugins {
let plugin_upper: String = plugin.name.to_uppercase().replace('.', "_");
let contract_impl: &str = plugin.implements.first().map(|s| s.as_str()).unwrap_or("");
let (contract_name, version_str): (&str, &str) = contract_impl
.split_once('@')
.unwrap_or((contract_impl, "1.0.0"));
let (version_major, version_minor_patch): (&str, &str) =
version_str.split_once('.').unwrap_or((version_str, "0"));
let version_minor: &str = version_minor_patch.split('.').next().unwrap_or("0");
let contract_name_full: String = format!("{}@{}", contract_name, version_major);
out.push_str(&format!(
" let desc_{plugin_upper}: PluginDescriptor = PluginDescriptor {{\n"
));
out.push_str(&format!(
" name: StringView {{ ptr: b\"{plugin_name}\".as_ptr(), len: {plugin_name_len}_usize }},\n",
plugin_name = plugin.name,
plugin_name_len = plugin.name.len()
));
out.push_str(&format!(
" contract_name: StringView {{ ptr: b\"{contract_name}\".as_ptr(), len: {contract_name_len}_usize }},\n",
contract_name = contract_name_full,
contract_name_len = contract_name_full.len()
));
out.push_str(&format!(
" version: Version {{ major: {version_major}, minor: {version_minor}, patch: 0 }},\n"
));
out.push_str(" };\n");
out.push_str(&format!(
" let mut err_{plugin_upper}: AbiError = AbiError {{ code: AbiErrorCode::Ok as u32, message: StringView::null() }};\n"
));
out.push_str(
" // SAFETY: desc and interface are 'static; &mut err is a valid out-param.\n",
);
out.push_str(" unsafe {\n");
out.push_str(&format!(
" (host.register_guest_contract)(host, &desc_{plugin_upper} as *const PluginDescriptor, &{plugin_upper}_INTERFACE as *const GuestContractInterface, &mut err_{plugin_upper});\n"
));
out.push_str(" };\n");
out.push_str(&format!(
" if err_{plugin_upper}.code != AbiErrorCode::Ok as u32 {{\n"
));
out.push_str(&format!(" return err_{plugin_upper};\n"));
out.push_str(" }\n\n");
}
} else {
for contract in &ir.contracts {
let upper: String = contract_name_to_upper_snake(&contract.name);
let plugin_name: String = contract.name.replace('.', "_") + "_plugin";
let plugin_name_len: usize = plugin_name.len();
let contract_name: String = format!("{}@{}", contract.name, contract.version.major);
let contract_name_len: usize = contract_name.len();
let major: u32 = contract.version.major;
let minor: u32 = contract.version.minor;
let patch: u32 = contract.version.patch;
out.push_str(&format!(
" let desc_{upper}: PluginDescriptor = PluginDescriptor {{\n"
));
out.push_str(&format!(
" name: StringView {{ ptr: b\"{plugin_name}\".as_ptr(), len: {plugin_name_len}_usize }},\n"
));
out.push_str(&format!(
" contract_name: StringView {{ ptr: b\"{contract_name}\".as_ptr(), len: {contract_name_len}_usize }},\n"
));
out.push_str(&format!(
" version: Version {{ major: {major}, minor: {minor}, patch: {patch} }},\n"
));
out.push_str(" };\n");
out.push_str(&format!(" let mut err_{upper}: AbiError = AbiError {{ code: AbiErrorCode::Ok as u32, message: StringView::null() }};\n"));
out.push_str(
" // SAFETY: desc and interface are 'static; &mut err is a valid out-param.\n",
);
out.push_str(" unsafe {\n");
out.push_str(&format!(
" (host.register_guest_contract)(host, &desc_{upper} as *const PluginDescriptor, &{upper}_INTERFACE as *const GuestContractInterface, &mut err_{upper});\n"
));
out.push_str(" };\n");
out.push_str(&format!(
" if err_{upper}.code != AbiErrorCode::Ok as u32 {{\n"
));
out.push_str(&format!(" return err_{upper};\n"));
out.push_str(" }\n\n");
}
}
out.push_str(" abi_error_ok()\n");
out.push_str("}\n");
}
fn needs_arg_pack(params: &[ResolvedParam]) -> bool {
if params.len() <= 1 {
return false;
}
true
}
fn emit_arg_pack_struct(out: &mut String, contract_struct: &str, func: &ResolvedFunction) {
let struct_name: String = arg_pack_struct_name(contract_struct, &func.name);
out.push_str(&format!(
"/// Auto-generated arg-pack for `{contract_struct}::{}`\n",
func.name
));
out.push_str("#[repr(C)]\n");
out.push_str("#[derive(Debug, Clone, Copy)]\n");
out.push_str(&format!("pub struct {struct_name} {{\n"));
for param in &func.params {
out.push_str(&format!(
" pub {}: {},\n",
param.name,
rust_type_name(¶m.ty)
));
}
out.push_str("}\n\n");
}
fn arg_pack_struct_name(contract_struct: &str, fn_name: &str) -> String {
let fn_pascal: String = fn_name
.split('_')
.map(|seg: &str| {
let mut chars: core::str::Chars<'_> = seg.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("");
format!("{contract_struct}{fn_pascal}Args")
}
fn dep_contract_name(dep: &ResolvedDependency) -> &str {
match dep {
ResolvedDependency::ByContract { contract, .. } => contract,
ResolvedDependency::ByBundle { contract, .. } => contract,
}
}
fn dep_contract_id(dep: &ResolvedDependency) -> u64 {
match dep {
ResolvedDependency::ByContract { contract_id, .. } => *contract_id,
ResolvedDependency::ByBundle { contract_id, .. } => *contract_id,
}
}
fn dep_min_version(dep: &ResolvedDependency) -> u32 {
match dep {
ResolvedDependency::ByContract { min_version, .. } => *min_version,
ResolvedDependency::ByBundle { min_version, .. } => *min_version,
}
}
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 generate_host_contract_caller(
out: &mut String,
contract: &ResolvedContract,
) -> Result<(), PolyplugcError> {
let struct_name: String = contract_name_to_struct(&contract.name);
let needs_arena: bool = contract_needs_arena(contract);
out.push_str(&format!(
"/// Host caller for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str("///\n");
out.push_str("/// RAII wrapper that manages instance lifecycle:\n");
out.push_str("/// - `new()`: calls `create_instance` on the resolved interface\n");
out.push_str("/// - `drop()`: calls `destroy_instance` to clean up\n");
out.push_str("/// - dispatch: passes `instance` to all method calls\n");
if needs_arena {
out.push_str("///\n");
out.push_str(
"/// # Call-arena lifetime\n\
///\n\
/// Methods returning variable-size values (`StringView`, `Buffer`, or structs\n\
/// that may embed one) take `&mut self` and reset this caller's arena at the\n\
/// start of the call. Any view returned by such a method borrows arena memory\n\
/// and is valid only until the next arena-backed call on the same caller.\n",
);
}
out.push_str(&format!("pub struct {struct_name} {{\n"));
out.push_str(" /// Resolved interface pointer from the registry.\n");
out.push_str(" interface: *const GuestContractInterface,\n");
out.push_str(" /// Instance handle created by `create_instance`.\n");
out.push_str(" instance: GuestContractInstance,\n");
out.push_str(" /// Host interface pointer (needed for create/destroy_instance).\n");
out.push_str(" host: *const HostApi,\n");
out.push_str(
" /// Contract handle, retained so the cache can re-resolve after a hot-reload\n\
\x20 /// (which swaps a new interface into the same slot) or report a gone contract.\n",
);
out.push_str(" handle: GuestContractHandle,\n");
out.push_str(
" /// Pointer to the runtime's registry revision counter, fetched once via\n\
\x20 /// `HostApi.revision_counter`. Polled directly before each dispatch (one\n\
\x20 /// atomic load, no call into the runtime); null when there is no runtime.\n",
);
out.push_str(" revision_ptr: *const u64,\n");
out.push_str(
" /// Revision value read when the interface was resolved. Compared before each\n\
\x20 /// dispatch against the live counter to detect a reload/unload and re-resolve,\n\
\x20 /// so the cached interface pointer never dangles.\n",
);
out.push_str(" cached_revision: u64,\n");
if needs_arena {
out.push_str(
" /// Stable-address backing buffer for the per-call arena. Boxed so the\n\
\x20 /// arena's interior pointers stay valid when the caller is moved.\n",
);
out.push_str(" arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]>,\n");
out.push_str(
" /// Per-call bump arena over `arena_buf`, reset at each arena-backed call.\n",
);
out.push_str(" arena: CallArena,\n");
}
out.push_str("}\n\n");
out.push_str(&format!("impl {struct_name} {{\n"));
out.push_str(" /// Create a new instance wrapper for this contract.\n");
out.push_str(" /// Calls `create_instance` on the resolved interface.\n");
out.push_str(" ///\n");
out.push_str(" /// # Arguments\n");
out.push_str(" /// - `handle`: Contract handle from `find_guest_contract`\n");
out.push_str(" /// - `host`: Host interface pointer\n");
out.push_str(" ///\n");
out.push_str(" /// # Returns\n");
out.push_str(" /// - `Some(Self)` if interface found and instance created\n");
out.push_str(" /// - `None` if interface not found or `create_instance` failed\n");
out.push_str(
" pub fn new(handle: GuestContractHandle, host: *const HostApi) -> Option<Self> {\n",
);
out.push_str(" // Resolve the interface from the handle via HostApi method\n");
out.push_str(" // SAFETY: host is reborrowed via as_ref() (returns None if null); resolve_guest_contract\n");
out.push_str(
" // is an ABI function pointer safe to call with a valid host and any handle.\n",
);
out.push_str(" let interface: *const GuestContractInterface = unsafe {\n");
out.push_str(" let iface: &HostApi = host.as_ref()?;\n");
out.push_str(" (iface.resolve_guest_contract)(host, handle)\n");
out.push_str(" };\n");
out.push_str(" if interface.is_null() {\n");
out.push_str(" return None;\n");
out.push_str(" }\n");
out.push_str(
" // Create instance via host-mediated lifecycle so the runtime tracks it.\n",
);
out.push_str(" // A null `instance.data` is valid: stateless contracts return a null\n");
out.push_str(
" // handle from `create_instance` and use it as an opaque dispatch token.\n",
);
out.push_str(
" let mut instance: GuestContractInstance = GuestContractInstance::null();\n",
);
out.push_str(" // SAFETY: host is reborrowed via as_ref() (returns None if null); create_guest_instance\n");
out.push_str(" // is an ABI function pointer that writes the new instance through the out-param.\n");
out.push_str(" unsafe {\n");
out.push_str(" let host_api: &HostApi = host.as_ref()?;\n");
out.push_str(
" (host_api.create_guest_instance)(host, interface, core::ptr::null(), &mut instance);\n",
);
out.push_str(" };\n");
out.push_str(
" // Fetch the registry revision counter ONCE, then read its current value, so\n\
\x20 // every later call can detect a reload/unload with a direct atomic load (no\n\
\x20 // call back into the runtime) and re-resolve before dispatching.\n",
);
out.push_str(
" // SAFETY: host is non-null here (resolve above reborrowed it via as_ref());\n",
);
out.push_str(
" // as_ref() re-guards null, and revision_counter is an ABI fn ptr safe to call.\n",
);
out.push_str(" let revision_ptr: *const u64 = unsafe {\n");
out.push_str(" match host.as_ref() {\n");
out.push_str(" Some(host_api) => (host_api.revision_counter)(host),\n");
out.push_str(" None => core::ptr::null(),\n");
out.push_str(" }\n");
out.push_str(" };\n");
out.push_str(" let cached_revision: u64 = if revision_ptr.is_null() {\n");
out.push_str(" 0\n");
out.push_str(" } else {\n");
out.push_str(
" // SAFETY: revision_ptr was returned by revision_counter and points to the\n",
);
out.push_str(" // runtime's revision counter (an AtomicU64), valid for the runtime's lifetime.\n");
out.push_str(" unsafe {\n");
out.push_str(" (*(revision_ptr as *const core::sync::atomic::AtomicU64))\n");
out.push_str(" .load(core::sync::atomic::Ordering::Acquire)\n");
out.push_str(" }\n");
out.push_str(" };\n");
if needs_arena {
out.push_str(
" // Box the backing buffer first so the arena's interior pointers refer\n",
);
out.push_str(
" // to a stable heap address that survives moving the caller value.\n",
);
out.push_str(" let mut arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]> = Box::new([0u8; CALL_ARENA_BUF_LEN]);\n");
out.push_str(
" let arena: CallArena = CallArena::new(arena_buf.as_mut_slice(), host);\n",
);
out.push_str(&format!(
" Some({struct_name} {{ interface, instance, host, handle, revision_ptr, cached_revision, arena_buf, arena }})\n"
));
} else {
out.push_str(&format!(
" Some({struct_name} {{ interface, instance, host, handle, revision_ptr, cached_revision }})\n"
));
}
out.push_str(" }\n\n");
out.push_str(
" /// Read the registry revision through the cached pointer — one aligned atomic\n\
\x20 /// load, no call into the runtime. Returns the cached value (i.e. \"unchanged\")\n\
\x20 /// when there is no counter (null host/runtime), so the staleness check is then\n\
\x20 /// a no-op.\n",
);
out.push_str(" #[inline]\n");
out.push_str(" fn live_revision(&self) -> u64 {\n");
out.push_str(" if self.revision_ptr.is_null() {\n");
out.push_str(" return self.cached_revision;\n");
out.push_str(" }\n");
out.push_str(
" // SAFETY: revision_ptr was returned by HostApi.revision_counter and points to\n",
);
out.push_str(" // the runtime's revision counter — an AtomicU64 whose address is stable for the\n");
out.push_str(" // runtime's lifetime, i.e. for as long as this caller can dispatch.\n");
out.push_str(" unsafe {\n");
out.push_str(" (*(self.revision_ptr as *const core::sync::atomic::AtomicU64))\n");
out.push_str(" .load(core::sync::atomic::Ordering::Acquire)\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" /// Check if this caller holds a resolved contract interface.\n");
out.push_str(" pub fn is_valid(&self) -> bool {\n");
out.push_str(" !self.interface.is_null()\n");
out.push_str(" }\n\n");
out.push_str(" /// Destroy current instance and create a new one.\n");
out.push_str(" /// Useful for recovering from plugin errors.\n");
out.push_str(" pub fn reset(&mut self) {\n");
out.push_str(
" // Route create/destroy through the host so the runtime's live-instance\n",
);
out.push_str(" // accounting stays accurate. If the host pointer is null there is no\n");
out.push_str(" // runtime to mediate the lifecycle, so leave the instance untouched.\n");
out.push_str(" // SAFETY: self.host is the stored host pointer; as_ref() returns None when it is null.\n");
out.push_str(" let host_api: &HostApi = match unsafe { self.host.as_ref() } {\n");
out.push_str(" Some(api) => api,\n");
out.push_str(" None => return,\n");
out.push_str(" };\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 self.live_revision() != self.cached_revision {\n");
out.push_str(" self.revalidate();\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" if !self.instance.data.is_null() {\n");
out.push_str(" // SAFETY: instance was created by create_guest_instance on this interface and is\n");
out.push_str(" // valid; destroy_guest_instance is an ABI function pointer safe to call with them.\n");
out.push_str(" unsafe {\n");
out.push_str(
" (host_api.destroy_guest_instance)(self.host, self.interface, self.instance);\n",
);
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(
" let mut new_instance: GuestContractInstance = GuestContractInstance::null();\n",
);
out.push_str(" // SAFETY: interface is valid for this wrapper; create_guest_instance writes the new\n");
out.push_str(" // instance through the out-param.\n");
out.push_str(" unsafe {\n");
out.push_str(
" (host_api.create_guest_instance)(self.host, self.interface, core::ptr::null(), &mut new_instance);\n",
);
out.push_str(" };\n");
out.push_str(" self.instance = new_instance;\n");
out.push_str(" }\n\n");
out.push_str(
" /// Re-resolve the cached interface after the registry changed under us.\n\
\x20 ///\n\
\x20 /// Invoked automatically when a dispatch observes a revision change. A\n\
\x20 /// hot-reload swapped a new interface into the same slot, so the retained\n\
\x20 /// handle still resolves — to the new interface; an unload vacated the slot,\n\
\x20 /// so it resolves to null and `false` is returned (the contract is gone).\n\
\x20 ///\n\
\x20 /// The old instance is ABANDONED, never destroyed: after a reload its\n\
\x20 /// interface — and the guest-side state that interface created — is already\n\
\x20 /// epoch-reclaimed, so calling the dead interface's `destroy_instance` would\n\
\x20 /// be undefined behaviour. The runtime reclaims the old instance's backing as\n\
\x20 /// part of the reload, then a fresh instance is created on the new interface.\n",
);
out.push_str(" fn revalidate(&mut self) -> bool {\n");
out.push_str(" // SAFETY: self.host is the stored host pointer; as_ref() returns None when it is null.\n");
out.push_str(" let host_api: &HostApi = match unsafe { self.host.as_ref() } {\n");
out.push_str(" Some(api) => api,\n");
out.push_str(" None => return false,\n");
out.push_str(" };\n");
out.push_str(" // SAFETY: resolve_guest_contract is an ABI fn ptr safe to call with a valid host and handle.\n");
out.push_str(" let interface: *const GuestContractInterface =\n");
out.push_str(
" unsafe { (host_api.resolve_guest_contract)(self.host, self.handle) };\n",
);
out.push_str(" if interface.is_null() {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(
" let mut instance: GuestContractInstance = GuestContractInstance::null();\n",
);
out.push_str(" // SAFETY: interface is freshly resolved and valid; create_guest_instance writes the new instance.\n");
out.push_str(" unsafe {\n");
out.push_str(" (host_api.create_guest_instance)(self.host, interface, core::ptr::null(), &mut instance);\n");
out.push_str(" }\n");
out.push_str(" self.interface = interface;\n");
out.push_str(" self.instance = instance;\n");
out.push_str(" self.cached_revision = self.live_revision();\n");
out.push_str(" true\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_host_fn_caller(out, func, contract, &struct_name)?;
}
out.push_str("}\n\n");
out.push_str(&format!("impl Drop for {struct_name} {{\n"));
out.push_str(" fn drop(&mut self) {\n");
if needs_arena {
out.push_str(
" // Free any overflow blocks the arena still holds before dropping.\n",
);
out.push_str(" self.arena.reset();\n");
}
out.push_str(
" // Destroy instance via host-mediated lifecycle so the runtime drops it\n",
);
out.push_str(
" // from its live-instance accounting. A null host pointer means there is\n",
);
out.push_str(" // no runtime to mediate the lifecycle, so skip the destroy.\n");
out.push_str(" // SAFETY: self.host is the stored host pointer; as_ref() returns None when it is null.\n");
out.push_str(" let host_api: &HostApi = match unsafe { self.host.as_ref() } {\n");
out.push_str(" Some(api) => api,\n");
out.push_str(" None => return,\n");
out.push_str(" };\n");
out.push_str(
" // If the registry changed since we resolved, the cached interface and instance\n",
);
out.push_str(
" // are stale — a reload/unload reclaimed their backing — so calling the dead\n",
);
out.push_str(
" // interface's destroy would be UB; the reload/unload already reclaimed the\n",
);
out.push_str(" // instance, so skip the destroy entirely.\n");
out.push_str(" if self.live_revision() != self.cached_revision {\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" if !self.instance.data.is_null() {\n");
out.push_str(
" // SAFETY: instance was created by create_guest_instance and is valid.\n",
);
out.push_str(
" // The interface pointer is stored for the lifetime of this wrapper.\n",
);
out.push_str(" unsafe {\n");
out.push_str(
" (host_api.destroy_guest_instance)(self.host, self.interface, self.instance);\n",
);
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
Ok(())
}
fn generate_host_fn_caller(
out: &mut String,
func: &ResolvedFunction,
_contract: &ResolvedContract,
contract_struct: &str,
) -> Result<(), PolyplugcError> {
let fn_id: u32 = func.function_id;
let sig_params: String = build_sig_params(func);
let ret_type: String = match &func.returns {
Some(ty) => rust_type_name(ty),
None => "()".to_owned(),
};
out.push_str(&format!(
" /// Call `{}` (function_id={})\n",
func.name, fn_id
));
out.push_str(" #[allow(clippy::absurd_extreme_comparisons)]\n");
let needs_arena: bool = fn_needs_arena(func);
if needs_arena {
out.push_str(
" /// Returns a value borrowing this caller's arena; it stays valid until\n\
\x20 /// the next arena-backed call on this caller.\n",
);
}
out.push_str(&format!(
" pub fn {}(&mut self{}) -> Result<{ret_type}, ContractError> {{\n",
func.name, sig_params
));
out.push_str(
" // Cheap per-call staleness check: read the registry revision directly through\n\
\x20 // the cached pointer (one atomic load, no call into the runtime). While it\n\
\x20 // matches the value cached when this caller resolved, the interface pointer is\n\
\x20 // current and we dispatch directly; on any change (hot-reload or unload) we\n\
\x20 // re-resolve first, so the cached pointer is never used once it dangles.\n",
);
out.push_str(
" if self.live_revision() != self.cached_revision && !self.revalidate() {\n",
);
out.push_str(" return Err(ContractError::new(AbiErrorCode::NotFound));\n");
out.push_str(" }\n");
emit_args_setup(out, func, contract_struct);
emit_out_setup(out, &func.returns);
let param_ty_comment: String = args_type_comment(func, contract_struct);
let ret_ty_comment: String = ret_type.clone();
out.push_str(&format!(
" // SAFETY: args_ptr points to a valid {param_ty_comment} and out_ptr to a valid {ret_ty_comment}.\n"
));
out.push_str(" // Enforced by the generated caller contract.\n");
out.push_str(" let out_ptr: *mut () = ");
if func.returns.is_some() {
out.push_str("out_val.as_mut_ptr() as *mut ();\n");
} else {
out.push_str("core::ptr::null_mut();\n");
}
emit_native_vm_dispatch(out, fn_id, needs_arena);
out.push_str(" if err.code != AbiErrorCode::Ok as u32 {\n");
out.push_str(" let message: String = if err.message.ptr.is_null() || err.message.len == 0 {\n");
out.push_str(" String::new()\n");
out.push_str(" } else {\n");
out.push_str(" // SAFETY: err.message.ptr is valid for err.message.len bytes and points to UTF-8 data.\n");
out.push_str(
" // The message is owned by the producer (static or runtime-owned); the\n",
);
out.push_str(
" // receiver must NEVER free it. We only copy it into an owned String.\n",
);
out.push_str(" let s: String = unsafe {\n");
out.push_str(" let slice: &[u8] = core::slice::from_raw_parts(err.message.ptr, err.message.len);\n");
out.push_str(" core::str::from_utf8_unchecked(slice).to_owned()\n");
out.push_str(" };\n");
out.push_str(" s\n");
out.push_str(" };\n");
out.push_str(" return Err(ContractError { code: AbiErrorCode::from_u32(err.code), message });\n");
out.push_str(" }\n");
if func.returns.is_some() {
emit_out_assume_init(out, &func.returns);
out.push_str(" Ok(out_val)\n");
} else {
out.push_str(" Ok(())\n");
}
out.push_str(" }\n\n");
Ok(())
}
fn emit_native_vm_dispatch(out: &mut String, fn_id: u32, needs_arena: bool) {
out.push_str(" // SAFETY: interface pointer is stored in wrapper, valid for the duration of the call.\n");
out.push_str(" let interface: &GuestContractInterface = unsafe { &*self.interface };\n");
out.push_str(
" // SAFETY: args_ptr/out_ptr match the ABI contract; instance is valid.\n",
);
out.push_str(" let mut err: AbiError = AbiError::ok();\n");
out.push_str(
" // SAFETY: args_ptr/out_ptr/&mut err match the ABI contract; instance is valid.\n",
);
out.push_str(" unsafe {\n");
out.push_str(" match interface.dispatch_type {\n");
out.push_str(" DispatchType::Native => {\n");
out.push_str(&format!(
" if {fn_id}_u32 >= interface.dispatch.native.function_count {{\n"
));
out.push_str(" err = AbiError { code: AbiErrorCode::FunctionNotAvailable as u32, message: polyplug_abi::string_view_from_static(b\"function not available in interface\") };\n");
out.push_str(" } else {\n");
out.push_str(&format!(
" let fn_ptr: *const () = *interface.dispatch.native.functions.add({fn_id}_usize);\n"
));
out.push_str(" // SAFETY: Transmuting *const () to a function pointer is sound because:\n");
out.push_str(" // - Function pointers have the same size and alignment as data pointers on all supported platforms\n");
out.push_str(" // - The interface guarantees that the function at this index is a native dispatch function\n");
out.push_str(" // with the exact signature: unsafe extern \"C\" fn(GuestContractInstance, *const (), *mut (), *mut AbiError)\n");
out.push_str(" let dispatch_fn: unsafe extern \"C\" fn(GuestContractInstance, *const (), *mut (), *mut AbiError) = core::mem::transmute(fn_ptr);\n");
out.push_str(
" dispatch_fn(self.instance, args_ptr, out_ptr, &mut err);\n",
);
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" DispatchType::VirtualMachine => {\n");
if needs_arena {
out.push_str(
" // Reset the per-call arena here — the VM path is the only one that\n\
\x20 // uses it. Rewinds the primary region and frees the previous call's\n\
\x20 // overflow blocks, invalidating any view returned by a prior call.\n",
);
out.push_str(" self.arena.reset();\n");
}
out.push_str(" (interface.dispatch.vm.call)(\n");
out.push_str(" interface.dispatch.vm.loader_data,\n");
out.push_str(" self.instance, // instance parameter\n");
out.push_str(&format!(" {fn_id}_u32,\n"));
out.push_str(" args_ptr,\n");
out.push_str(" out_ptr,\n");
if needs_arena {
out.push_str(" &mut self.arena as *mut CallArena,\n");
} else {
out.push_str(" core::ptr::null_mut(),\n");
}
out.push_str(" &mut err,\n");
out.push_str(" );\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" };\n");
}
fn build_sig_params(func: &ResolvedFunction) -> String {
if func.params.is_empty() {
return String::new();
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let ty: &ResolvedTypeRef = ¶m.ty;
return match ty {
ResolvedTypeRef::UserDefined(_) => {
format!(", {}: &{}", param.name, rust_type_name(ty))
}
_ => format!(", {}: {}", param.name, rust_type_name(ty)),
};
}
let params_str: String = func
.params
.iter()
.map(|p: &ResolvedParam| format!(", {}: {}", p.name, rust_type_name(&p.ty)))
.collect::<Vec<_>>()
.join("");
params_str
}
fn emit_args_setup(out: &mut String, func: &ResolvedFunction, contract_struct: &str) {
if func.params.is_empty() {
out.push_str(" let args_ptr: *const () = core::ptr::null();\n");
return;
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let ty_name: String = rust_type_name(¶m.ty);
match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" let args_ptr: *const () = {name} as *const {ty} as *const ();\n",
name = param.name,
ty = ty_name,
));
}
_ => {
out.push_str(&format!(
" let {name}_val: {ty} = {name};\n",
name = param.name,
ty = ty_name,
));
out.push_str(&format!(
" let args_ptr: *const () = &{name}_val as *const {ty} as *const ();\n",
name = param.name,
ty = ty_name,
));
}
}
return;
}
let pack_struct: String = arg_pack_struct_name(contract_struct, &func.name);
out.push_str(&format!(
" let args_val: {pack_struct} = {pack_struct} {{\n"
));
for param in &func.params {
out.push_str(&format!(" {}: {},\n", param.name, param.name));
}
out.push_str(" };\n");
out.push_str(&format!(
" let args_ptr: *const () = &args_val as *const {pack_struct} as *const ();\n"
));
}
fn emit_out_setup(out: &mut String, returns: &Option<ResolvedTypeRef>) {
if let Some(ret_ty) = returns {
let ty_name: String = rust_type_name(ret_ty);
out.push_str(&format!(
" let mut out_val: core::mem::MaybeUninit<{ty_name}> = core::mem::MaybeUninit::uninit();\n"
));
}
}
fn emit_out_assume_init(out: &mut String, returns: &Option<ResolvedTypeRef>) {
if let Some(ret_ty) = returns {
let ty_name: String = rust_type_name(ret_ty);
out.push_str(
" // SAFETY: dispatch returned AbiErrorCode::Ok, so it wrote a valid value\n",
);
out.push_str(" // through the out-pointer into this slot.\n");
out.push_str(&format!(
" let out_val: {ty_name} = unsafe {{ out_val.assume_init() }};\n"
));
}
}
fn args_type_comment(func: &ResolvedFunction, contract_struct: &str) -> String {
if func.params.is_empty() {
return "()".to_owned();
}
if func.params.len() == 1 {
return rust_type_name(&func.params[0].ty);
}
arg_pack_struct_name(contract_struct, &func.name)
}
fn generate_rust_type(out: &mut String, ty: &ResolvedType) {
out.push_str(&format!("/// User-defined type `{}`\n", ty.name));
out.push_str("#[repr(C)]\n");
out.push_str("#[derive(Debug, Clone, Copy)]\n");
out.push_str(&format!("pub struct {} {{\n", ty.name));
for field in &ty.fields {
out.push_str(&format!(
" pub {}: {},\n",
field.name,
rust_type_name(&field.ty)
));
}
out.push_str("}\n\n");
}
fn rust_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.rust_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Buffer".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "*mut ()".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "()".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn to_snake_case(s: &str) -> String {
let mut result: String = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.extend(c.to_lowercase());
}
result
}
fn substitute_variant_refs_rust_enum(
declared_variants: &[EnumVariant],
expr: &str,
_enum_name: &str,
repr_type: &str,
) -> String {
let declared_names: Vec<&str> = declared_variants.iter().map(|v| v.name.as_str()).collect();
let chars: Vec<char> = expr.chars().collect();
let len: usize = chars.len();
let mut result: String = String::new();
let mut i: usize = 0;
while i < len {
let c: char = chars[i];
if c.is_alphabetic() || c == '_' {
let start: usize = i;
while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let ident: String = chars[start..i].iter().collect();
if declared_names.contains(&ident.as_str()) {
result.push_str(&format!("Self::{} as {}", ident, repr_type));
} else {
result.push_str(&ident);
}
} else {
result.push(c);
i += 1;
}
}
result
}
fn substitute_variant_refs_rust_bitflag(declared_variants: &[EnumVariant], expr: &str) -> String {
let declared_names: Vec<&str> = declared_variants.iter().map(|v| v.name.as_str()).collect();
let chars: Vec<char> = expr.chars().collect();
let len: usize = chars.len();
let mut result: String = String::new();
let mut i: usize = 0;
while i < len {
let c: char = chars[i];
if c.is_alphabetic() || c == '_' {
let start: usize = i;
while i < len && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let ident: String = chars[start..i].iter().collect();
if declared_names.contains(&ident.as_str()) {
result.push_str(&ident.to_uppercase());
} else {
result.push_str(&ident);
}
} else {
result.push(c);
i += 1;
}
}
result
}
fn generate_rust_enum(out: &mut String, e: &EnumDef) {
let repr_str: &str = e.repr.rust_name();
if e.bitflag {
let mod_name: String = to_snake_case(&e.name);
out.push_str(&format!(
"/// Bitflag enum `{}` (repr {})\n",
e.name, repr_str
));
out.push_str(&format!("pub mod {} {{\n", mod_name));
out.push_str(&format!(" pub type {} = {};\n", e.name, repr_str));
for variant in &e.variants {
let subst_value: String =
substitute_variant_refs_rust_bitflag(&e.variants, &variant.value);
out.push_str(&format!(
" pub const {}: {} = {};\n",
variant.name.to_uppercase(),
e.name,
subst_value
));
}
out.push_str("}\n");
out.push_str(&format!("pub use {}::{};\n\n", mod_name, e.name));
} else {
out.push_str(&format!("/// Enum `{}` (repr {})\n", e.name, repr_str));
out.push_str(&format!("#[repr({})]\n", repr_str));
out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n");
out.push_str(&format!("pub enum {} {{\n", e.name));
for variant in &e.variants {
let subst_value: String =
substitute_variant_refs_rust_enum(&e.variants, &variant.value, &e.name, repr_str);
out.push_str(&format!(" {} = {},\n", variant.name, subst_value));
}
out.push_str("}\n\n");
}
}
fn contract_name_to_struct(name: &str) -> String {
name.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("")
+ "Contract"
}
fn host_contract_name_to_trait(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
} else {
"Host".to_owned() + &pascal
}
}
fn host_contract_name_to_caller(name: &str) -> String {
host_contract_name_to_trait(name) + "Caller"
}
fn host_param_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.rust_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "&str".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "&[u8]".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "*mut ()".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "()".to_owned(),
ResolvedTypeRef::UserDefined(name) => format!("&{name}"),
}
}
fn host_return_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.rust_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "String".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Vec<u8>".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "*mut ()".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "()".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn generate_host_contract_trait(out: &mut String, contract: &ResolvedHostContract) {
let trait_name: String = host_contract_name_to_trait(&contract.name);
out.push_str(&format!(
"/// Host trait for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str("/// Hosts implement this trait to provide functionality to plugins.\n");
out.push_str(&format!("pub trait {trait_name}: Send + Sync {{\n"));
for func in &contract.functions {
generate_host_trait_method(out, func);
}
out.push_str("}\n\n");
}
fn generate_host_trait_method(out: &mut String, func: &ResolvedFunction) {
let params_str: String = if func.params.is_empty() {
String::new()
} else {
func.params
.iter()
.map(|p: &ResolvedParam| format!(", {}: {}", p.name, host_param_type_name(&p.ty)))
.collect::<Vec<_>>()
.join("")
};
let ret_type: String = match &func.returns {
Some(ty) => host_return_type_name(ty),
None => "()".to_owned(),
};
out.push_str(&format!(
" fn {}(&self{}) -> {ret_type};\n",
func.name, params_str
));
}
fn generate_host_contracts_file(ir: &ValidatedIr) -> String {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --api api.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut out: String = String::new();
out.push_str(header);
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::Buffer;\n");
out.push_str("use super::types::*;\n\n");
for contract in &ir.host_contracts {
generate_host_contract_trait(&mut out, contract);
}
for contract in &ir.host_contracts {
let trait_name: String = host_contract_name_to_trait(&contract.name);
let const_name: String = trait_name.to_uppercase() + "_CONTRACT_ID";
out.push_str(&format!(
"/// Contract ID constant for `{}` (FNV-1a of \"host_contract:{}@{}\")\n",
contract.name, contract.name, contract.version.major
));
out.push_str(&format!(
"pub const {const_name}: u64 = 0x{:016X};\n\n",
contract.contract_id
));
}
out
}
fn generate_host_interface_factories_file(ir: &ValidatedIr) -> String {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --api api.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut out: String = String::new();
out.push_str(header);
out.push_str("use polyplug_abi::HostContractInterface;\n");
out.push_str("use polyplug_abi::HostContractInstance;\n");
out.push_str("use polyplug_abi::HostApi;\n");
out.push_str("use polyplug_abi::GuestContractInstance;\n");
out.push_str("use polyplug_abi::VmLoaderData;\n");
out.push_str("use polyplug_abi::DispatchMechanisms;\n");
out.push_str("use polyplug_abi::NativeDispatch;\n");
out.push_str("use polyplug_abi::VmDispatch;\n");
out.push_str("use polyplug_abi::CallArena;\n");
out.push_str("use polyplug_abi::DispatchType;\n");
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::AbiError;\n");
out.push_str("use polyplug_abi::AbiErrorCode;\n");
out.push_str("use polyplug_abi::abi_error_ok;\n");
out.push_str("use polyplug_abi::string_view_from_static;\n");
out.push_str("use polyplug_abi::Version;\n");
out.push_str("use polyplug_utils::HostContractId;\n");
out.push_str("use core::ffi::c_void;\n");
out.push_str("use super::host_contracts::*;\n");
out.push_str("use super::types::*;\n\n");
for contract in &ir.host_contracts {
generate_host_interface_factory(&mut out, contract);
}
out
}
fn generate_host_interface_factory(out: &mut String, contract: &ResolvedHostContract) {
let trait_name: String = host_contract_name_to_trait(&contract.name);
let factory_name: String = format!(
"create_{}_interface",
contract.name.replace('.', "_").to_lowercase()
);
let factory_vm_name: String = format!(
"create_{}_interface_vm",
contract.name.replace('.', "_").to_lowercase()
);
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;
out.push_str(&format!(
"/// Create a host contract interface for `{}` with NATIVE dispatch.\n",
contract.name
));
out.push_str("///\n");
out.push_str("/// Takes ownership of the implementation and creates a 'static interface.\n");
out.push_str("/// The implementation must be `Send + Sync`.\n");
out.push_str("///\n");
out.push_str("/// # Memory\n");
out.push_str(
"/// The returned interface is leaked and lives for the lifetime of the program.\n",
);
out.push_str(
"/// The implementation Box is also leaked (its pointer is stored in the interface).\n",
);
out.push_str(&format!(
"pub fn {factory_name}(implementation: Box<dyn {trait_name}>) -> &'static HostContractInterface {{\n"
));
out.push_str(&format!(
" let fat_ptr: *const dyn {trait_name} = Box::into_raw(implementation);\n"
));
out.push_str(&format!(
" let wrapper: Box<*const dyn {trait_name}> = Box::new(fat_ptr);\n"
));
out.push_str(&format!(
" let impl_ptr: *const *const dyn {trait_name} = Box::into_raw(wrapper);\n\n"
));
for func in &contract.functions {
generate_host_thunk(out, func, &contract.name, &trait_name);
}
out.push_str(&format!(
" static FUNCTIONS: [unsafe extern \"C\" fn(*const c_void, *const (), *mut (), *mut AbiError); {fn_count}] = [\n"
));
for func in &contract.functions {
let thunk_name: String = format!(
"{}_{}_thunk",
contract.name.replace('.', "_").to_lowercase(),
func.name
);
out.push_str(&format!(" {thunk_name},\n"));
}
out.push_str(" ];\n\n");
let create_stub_name: String = format!(
"{}_create_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
let destroy_stub_name: String = format!(
"{}_destroy_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
let singleton: bool = contract.singleton;
out.push_str(&format!(
" /// Create instance stub for `{}` host contract.\n",
contract.name
));
out.push_str(" /// For host contracts, the instance is the implementation object.\n");
out.push_str(
" /// This stub returns the registrant-owned `user_data` as the instance data.\n",
);
out.push_str(&format!(" unsafe extern \"C\" fn {create_stub_name}(\n"));
out.push_str(" this: *const HostContractInterface,\n");
out.push_str(" _args: *const (),\n");
out.push_str(" out_instance: *mut HostContractInstance,\n");
out.push_str(" ) {\n");
out.push_str(" if out_instance.is_null() { return; }\n");
out.push_str(
" // SAFETY: `this` is a valid HostContractInterface pointer per ABI contract;\n",
);
out.push_str(
" // `user_data` holds the implementation pointer stored at registration, and\n",
);
out.push_str(
" // `out_instance` is a valid, writable out-param per the ABI contract.\n",
);
out.push_str(" unsafe { out_instance.write(HostContractInstance { data: (*this).user_data }); }\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" /// Destroy instance stub for `{}` host contract.\n",
contract.name
));
out.push_str(" /// For singleton contracts, this is a no-op.\n");
out.push_str(
" /// For multi-instance contracts, the host must provide a custom destructor.\n",
);
out.push_str(&format!(
" unsafe extern \"C\" fn {destroy_stub_name}(\n"
));
out.push_str(" _this: *const HostContractInterface,\n");
out.push_str(" _instance: HostContractInstance,\n");
out.push_str(" ) {\n");
if singleton {
out.push_str(
" // Singleton: no cleanup needed, implementation lives for program lifetime\n",
);
} else {
out.push_str(" // Multi-instance: host should provide custom destroy_instance\n");
}
out.push_str(" }\n\n");
let patch: u32 = contract.version.patch;
out.push_str(" let interface: HostContractInterface = HostContractInterface {\n");
out.push_str(&format!(
" contract_id: HostContractId::from(0x{contract_id:016X}_u64),\n"
));
out.push_str(&format!(
" contract_version: Version {{ major: {major}, minor: {minor}, patch: {patch} }},\n"
));
out.push_str(&format!(" singleton: {singleton},\n"));
out.push_str(" dispatch_type: DispatchType::Native,\n");
out.push_str(" runtime: core::ptr::null_mut(),\n");
out.push_str(" user_data: impl_ptr as *mut c_void,\n");
out.push_str(&format!(" create_instance: {create_stub_name},\n"));
out.push_str(&format!(" destroy_instance: {destroy_stub_name},\n"));
out.push_str(" dispatch: DispatchMechanisms {\n");
out.push_str(" native: NativeDispatch {\n");
out.push_str(&format!(
" function_count: {fn_count}_u32,\n"
));
out.push_str(" functions: FUNCTIONS.as_ptr() as *const *const (),\n");
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n\n");
out.push_str(" Box::leak(Box::new(interface))\n");
out.push_str("}\n\n");
out.push_str(&format!(
"/// Create a host contract interface for `{}` with VM dispatch.\n",
contract.name
));
out.push_str("///\n");
out.push_str("/// Used when the host implementation is in a VM language (Python, Lua, JS).\n");
out.push_str("///\n");
out.push_str("/// # Arguments\n");
out.push_str("/// * `bridge_data` - Opaque pointer to VM-specific data\n");
out.push_str("/// * `dispatch_fn` - Function to call for each contract function\n");
out.push_str("///\n");
out.push_str("/// # Memory\n");
out.push_str(
"/// The returned interface is leaked and lives for the lifetime of the program.\n",
);
out.push_str(&format!("pub fn {factory_vm_name}(\n"));
out.push_str(" bridge_data: *mut c_void,\n");
out.push_str(" dispatch_fn: unsafe extern \"C\" fn(\n");
out.push_str(" loader_data: VmLoaderData,\n");
out.push_str(" instance: GuestContractInstance,\n");
out.push_str(" fn_id: u32,\n");
out.push_str(" args: *const (),\n");
out.push_str(" out: *mut (),\n");
out.push_str(" arena: *mut CallArena,\n");
out.push_str(" out_err: *mut AbiError,\n");
out.push_str(" ),\n");
out.push_str(") -> &'static HostContractInterface {\n");
let vm_create_stub_name: String = format!(
"{}_vm_create_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
let vm_destroy_stub_name: String = format!(
"{}_vm_destroy_instance_stub",
contract.name.replace('.', "_").to_lowercase()
);
out.push_str(&format!(
" /// Create instance stub for `{}` host contract (VM dispatch).\n",
contract.name
));
out.push_str(" /// Returns the registrant-owned `user_data` (bridge_data) as instance for VM-based implementations.\n");
out.push_str(&format!(
" unsafe extern \"C\" fn {vm_create_stub_name}(\n"
));
out.push_str(" this: *const HostContractInterface,\n");
out.push_str(" _args: *const (),\n");
out.push_str(" out_instance: *mut HostContractInstance,\n");
out.push_str(" ) {\n");
out.push_str(" if out_instance.is_null() { return; }\n");
out.push_str(
" // SAFETY: `this` is a valid HostContractInterface pointer per ABI contract;\n",
);
out.push_str(
" // `user_data` holds the bridge_data stored at registration, and `out_instance`\n",
);
out.push_str(" // is a valid, writable out-param per the ABI contract.\n");
out.push_str(" unsafe { out_instance.write(HostContractInstance { data: (*this).user_data }); }\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" /// Destroy instance stub for `{}` host contract (VM dispatch).\n",
contract.name
));
out.push_str(&format!(
" unsafe extern \"C\" fn {vm_destroy_stub_name}(\n"
));
out.push_str(" _this: *const HostContractInterface,\n");
out.push_str(" _instance: HostContractInstance,\n");
out.push_str(" ) {\n");
if singleton {
out.push_str(" // Singleton: no cleanup needed\n");
} else {
out.push_str(" // Multi-instance: host should provide custom destroy_instance\n");
}
out.push_str(" }\n\n");
out.push_str(" let interface: HostContractInterface = HostContractInterface {\n");
out.push_str(&format!(
" contract_id: HostContractId::from(0x{contract_id:016X}_u64),\n"
));
out.push_str(&format!(
" contract_version: Version {{ major: {major}, minor: {minor}, patch: {patch} }},\n"
));
out.push_str(&format!(" singleton: {singleton},\n"));
out.push_str(" dispatch_type: DispatchType::VirtualMachine,\n");
out.push_str(" runtime: core::ptr::null_mut(),\n");
out.push_str(" user_data: bridge_data,\n");
out.push_str(&format!(
" create_instance: {vm_create_stub_name},\n"
));
out.push_str(&format!(
" destroy_instance: {vm_destroy_stub_name},\n"
));
out.push_str(" dispatch: DispatchMechanisms {\n");
out.push_str(" vm: VmDispatch {\n");
out.push_str(" call: dispatch_fn,\n");
out.push_str(" loader_data: VmLoaderData { data: bridge_data },\n");
out.push_str(" },\n");
out.push_str(" },\n");
out.push_str(" };\n\n");
out.push_str(" Box::leak(Box::new(interface))\n");
out.push_str("}\n\n");
}
fn generate_host_thunk(
out: &mut String,
func: &ResolvedFunction,
contract_name: &str,
trait_name: &str,
) {
let thunk_name: String = format!(
"{}_{}_thunk",
contract_name.replace('.', "_").to_lowercase(),
func.name
);
let has_return: bool = func.returns.is_some();
out.push_str(
" // SAFETY: impl_ptr is a valid *const T from Box::into_raw, args/out are valid per ABI contract.\n"
);
out.push_str(&format!(" unsafe extern \"C\" fn {thunk_name}(\n"));
out.push_str(" impl_ptr: *const c_void,\n");
out.push_str(" args: *const (),\n");
out.push_str(" out: *mut (),\n");
out.push_str(" out_err: *mut AbiError,\n");
out.push_str(" ) {\n");
out.push_str(" let __thunk_err: AbiError = match std::panic::catch_unwind(core::panic::AssertUnwindSafe(|| {\n");
out.push_str(&format!(
" let fat_ptr: *const *const dyn {trait_name} = impl_ptr as *const *const dyn {trait_name};\n"
));
out.push_str(
" // SAFETY: impl_ptr came from Box::into_raw(Box::new(fat_ptr)); the wrapper Box is leaked\n",
);
out.push_str(
" // for the program's lifetime and the inner fat pointer has a valid vtable.\n",
);
out.push_str(&format!(
" let impl_ref: &dyn {trait_name} = unsafe {{ &**fat_ptr }};\n"
));
if !func.params.is_empty() {
generate_host_thunk_args(out, func, trait_name);
} else {
out.push_str(" let _ = args;\n");
}
generate_host_thunk_call(out, func, has_return);
if has_return {
let ret_ty: String = match func.returns.as_ref() {
Some(ret) => host_return_type_name(ret),
None => String::from("()"),
};
out.push_str(&format!(
" // SAFETY: out is a valid *mut {ret_ty} per ABI contract.\n"
));
out.push_str(&format!(
" unsafe {{ core::ptr::write(out as *mut {ret_ty}, result); }}\n"
));
} else {
out.push_str(" let _ = out;\n");
}
out.push_str(" abi_error_ok()\n");
out.push_str(" })) {\n");
out.push_str(" Ok(err) => err,\n");
out.push_str(" Err(_) => AbiError {\n");
out.push_str(" code: AbiErrorCode::Panic as u32,\n");
out.push_str(&format!(
" message: string_view_from_static(b\"panic in host.{}::{}\"),\n",
trait_name.to_lowercase().replace("host", ""),
func.name
));
out.push_str(" },\n");
out.push_str(" };\n");
out.push_str(" if !out_err.is_null() {\n");
out.push_str(
" // SAFETY: out_err is a valid, writable *mut AbiError per the ABI contract.\n",
);
out.push_str(" unsafe { out_err.write(__thunk_err); }\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
}
fn generate_host_thunk_args(out: &mut String, func: &ResolvedFunction, trait_name: &str) {
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let ty_name: String = host_abi_type_name(¶m.ty);
out.push_str(&format!(
" // SAFETY: args is a valid *const {ty_name} per ABI contract.\n"
));
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" let {name}_sv: StringView = unsafe {{ *(args as *const StringView) }};\n",
name = param.name
));
out.push_str(&format!(
" // SAFETY: as_str handles null/empty; UTF-8 trusted per TRUST_MODEL.\n let {name}: &str = unsafe {{ {name}_sv.as_str() }};\n",
name = param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" let {name}_buf: polyplug_abi::Buffer = unsafe {{ *(args as *const polyplug_abi::Buffer) }};\n",
name = param.name
));
out.push_str(&format!(
" // SAFETY: as_slice handles null/empty; host owns the bytes per TRUST_MODEL.\n let {name}: &[u8] = unsafe {{ {name}_buf.as_slice() }};\n",
name = param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" let {name}: &{ty} = unsafe {{ &*(args as *const {ty}) }};\n",
name = param.name,
ty = ty_name
));
}
_ => {
out.push_str(&format!(
" let {name}: {ty} = unsafe {{ *(args as *const {ty}) }};\n",
name = param.name,
ty = ty_name
));
}
}
} else {
let pack_struct: String = arg_pack_struct_name(trait_name, &func.name);
out.push_str(&format!(
" // SAFETY: args is a valid *const {pack_struct} per ABI contract.\n"
));
out.push_str(&format!(
" let packed: &{pack_struct} = unsafe {{ &*(args as *const {pack_struct}) }};\n"
));
for param in &func.params {
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" // SAFETY: as_str handles null/empty; UTF-8 trusted per TRUST_MODEL.\n let {name}: &str = unsafe {{ packed.{name}.as_str() }};\n",
name = param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" // SAFETY: as_slice handles null/empty; host owns the bytes per TRUST_MODEL.\n let {name}: &[u8] = unsafe {{ packed.{name}.as_slice() }};\n",
name = param.name
));
}
_ => {
let ty: String = host_param_type_name(¶m.ty);
let is_ref: bool = ty.starts_with('&');
if is_ref {
out.push_str(&format!(
" let {name}: {ty} = &packed.{name};\n",
name = param.name,
ty = ty
));
} else {
out.push_str(&format!(
" let {name}: {ty} = packed.{name};\n",
name = param.name,
ty = ty
));
}
}
}
}
}
}
fn generate_host_thunk_call(out: &mut String, func: &ResolvedFunction, has_return: bool) {
let call_args: String = if func.params.is_empty() {
String::new()
} else if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => format!(", {}", param.name),
_ => format!(", {}", param.name),
}
} else {
func.params
.iter()
.map(|p: &ResolvedParam| format!(", {}", p.name))
.collect::<Vec<_>>()
.join("")
};
if has_return {
let ret_ty: String = match func.returns.as_ref() {
Some(ret) => host_return_type_name(ret),
None => String::from("()"),
};
out.push_str(&format!(
" let result: {} = impl_ref.{}({});\n",
ret_ty,
func.name,
call_args.trim_start_matches(", ")
));
} else {
out.push_str(&format!(
" impl_ref.{}({});\n",
func.name,
call_args.trim_start_matches(", ")
));
}
}
fn host_abi_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.rust_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "polyplug_abi::Buffer".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "*mut ()".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "()".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn generate_guest_host_contracts_file(ir: &ValidatedIr) -> String {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --bundle bundle.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut out: String = String::new();
out.push_str(header);
let any_needs_arena: bool = ir
.host_contracts
.iter()
.any(|c: &ResolvedHostContract| c.functions.iter().any(fn_needs_arena));
out.push_str("use polyplug_abi::HostApi;\n");
out.push_str("use polyplug_abi::HostContractInterface;\n");
out.push_str("use polyplug_abi::HostContractInstance;\n");
out.push_str("use polyplug_abi::GuestContractInstance;\n");
out.push_str("use polyplug_abi::DispatchType;\n");
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::Buffer;\n");
out.push_str("use polyplug_abi::AbiError;\n");
out.push_str("use polyplug_abi::AbiErrorCode;\n");
out.push_str("use polyplug_abi::string_view_null;\n");
out.push_str("use polyplug_guest::HostContext;\n");
if any_needs_arena {
out.push_str("use polyplug_abi::CallArena;\n");
}
out.push_str("use core::ffi::c_void;\n");
out.push_str("use super::types::*;\n\n");
if any_needs_arena {
out.push_str("const CALL_ARENA_BUF_LEN: usize = 512;\n\n");
}
out.push_str("/// Error type for host contract calls from guest.\n");
out.push_str("#[derive(Debug)]\n");
out.push_str("pub struct HostContractError {\n");
out.push_str(" /// ABI error code (non-zero).\n");
out.push_str(" pub code: AbiErrorCode,\n");
out.push_str(" /// Human-readable error message (may be empty).\n");
out.push_str(" pub message: String,\n");
out.push_str("}\n\n");
out.push_str("impl HostContractError {\n");
out.push_str(" /// Create a new error with the given code.\n");
out.push_str(" pub fn new(code: AbiErrorCode) -> Self {\n");
out.push_str(" Self { code, message: String::new() }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
for contract in &ir.host_contracts {
generate_guest_host_contract_caller(&mut out, contract);
}
for contract in &ir.host_contracts {
let trait_name: String = host_contract_name_to_trait(&contract.name);
let const_name: String = trait_name.to_uppercase() + "_CONTRACT_ID";
out.push_str(&format!(
"/// Contract ID constant for `{}` (FNV-1a of \"host_contract:{}@{}\")\n",
contract.name, contract.name, contract.version.major
));
out.push_str(&format!(
"pub const {const_name}: u64 = 0x{:016X};\n\n",
contract.contract_id
));
}
out
}
fn generate_guest_host_contract_caller(out: &mut String, contract: &ResolvedHostContract) {
let caller_name: String = host_contract_name_to_caller(&contract.name);
let needs_arena: bool = contract.functions.iter().any(fn_needs_arena);
out.push_str(&format!(
"/// Guest caller for host contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
if needs_arena {
out.push_str("///\n");
out.push_str(
"/// # Call-arena lifetime\n\
///\n\
/// Methods returning variable-size values (`StringView`, `Buffer`, or structs\n\
/// that may embed one) take `&mut self` and reset this caller's arena at the\n\
/// start of the call. Any view returned by such a method borrows arena memory\n\
/// and is valid only until the next arena-backed call on the same caller.\n",
);
}
out.push_str(&format!("pub struct {caller_name} {{\n"));
out.push_str(
" /// Vtable for the host contract: provides dispatch metadata and function pointers.\n",
);
out.push_str(" /// Resolved via `HostApi::resolve_host_contract_interface`.\n");
out.push_str(" interface: *const HostContractInterface,\n");
out.push_str(" /// Per-instance state for the host contract, produced by\n");
out.push_str(" /// `HostApi::get_host_contract`. Passed as the first argument to native\n");
out.push_str(" /// dispatch functions (the host thunk dereferences it as the implementation pointer).\n");
out.push_str(" instance: HostContractInstance,\n");
out.push_str(
" /// Host interface pointer captured in `from_host` — used for cross-boundary\n",
);
out.push_str(" /// string allocation when marshalling StringView parameters.\n");
out.push_str(" host: *const HostApi,\n");
if needs_arena {
out.push_str(
" /// Stable-address backing buffer for the per-call arena. Boxed so the\n\
\x20 /// arena's interior pointers stay valid when the caller is moved.\n",
);
out.push_str(" arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]>,\n");
out.push_str(
" /// Per-call bump arena over `arena_buf`, reset at each arena-backed call.\n",
);
out.push_str(" arena: CallArena,\n");
}
out.push_str("}\n\n");
out.push_str(&format!("impl {caller_name} {{\n"));
out.push_str(" /// Factory method - creates caller from HostApi or None if not found.\n");
out.push_str(" ///\n");
out.push_str(" /// # Safety\n");
out.push_str(" /// The `host` pointer must be valid and non-null.\n");
out.push_str(
" pub unsafe fn from_host(host: *const HostApi, min_version: u32) -> Option<Self> {\n",
);
out.push_str(" if host.is_null() {\n");
out.push_str(" return None;\n");
out.push_str(" }\n");
out.push_str(" // SAFETY: host is non-null and valid per ABI contract.\n");
out.push_str(" let host: &HostApi = unsafe { &*host };\n");
out.push_str(
" // Resolve the contract vtable. This is the source of dispatch metadata\n",
);
out.push_str(" // (dispatch_type, function_count, functions) — NOT the instance.\n");
out.push_str(" // SAFETY: `host` is the reborrowed non-null HostApi; resolve_host_contract_interface\n");
out.push_str(" // is an ABI function pointer safe to call with a valid host (returns null if absent).\n");
out.push_str(" let interface: *const HostContractInterface = unsafe {\n");
out.push_str(&format!(
" (host.resolve_host_contract_interface)(host, 0x{:016X}_u64, min_version)\n",
contract.contract_id
));
out.push_str(" };\n");
out.push_str(" if interface.is_null() {\n");
out.push_str(" return None;\n");
out.push_str(" }\n");
out.push_str(
" // Obtain the per-instance state. Native dispatch functions receive this\n",
);
out.push_str(" // pointer as their first argument; the host thunk dereferences it.\n");
out.push_str(" // SAFETY: `host` is the reborrowed non-null HostApi; get_host_contract is an ABI\n");
out.push_str(" // function pointer safe to call with a valid host (null-data instance if absent).\n");
out.push_str(" let instance: HostContractInstance = unsafe {\n");
out.push_str(&format!(
" (host.get_host_contract)(host, 0x{:016X}_u64, min_version)\n",
contract.contract_id
));
out.push_str(" };\n");
if needs_arena {
out.push_str(
" // Box the backing buffer first so the arena's interior pointers refer\n",
);
out.push_str(
" // to a stable heap address that survives moving the caller value.\n",
);
out.push_str(" let mut arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]> = Box::new([0u8; CALL_ARENA_BUF_LEN]);\n");
out.push_str(
" let arena: CallArena = CallArena::new(arena_buf.as_mut_slice(), host);\n",
);
out.push_str(&format!(
" Some({caller_name} {{ interface, instance, host: host as *const HostApi, arena_buf, arena }})\n"
));
} else {
out.push_str(&format!(
" Some({caller_name} {{ interface, instance, host: host as *const HostApi }})\n"
));
}
out.push_str(" }\n\n");
out.push_str(" /// Check if caller is valid (resolved interface is non-null).\n");
out.push_str(" pub fn is_valid(&self) -> bool {\n");
out.push_str(" !self.interface.is_null()\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_guest_host_contract_method(out, func, &caller_name);
}
out.push_str("}\n\n");
}
fn generate_guest_host_contract_method(
out: &mut String,
func: &ResolvedFunction,
caller_name: &str,
) {
let fn_id: u32 = func.function_id;
let needs_arena: bool = fn_needs_arena(func);
let params_str: String = if func.params.is_empty() {
String::new()
} else {
func.params
.iter()
.map(|p: &ResolvedParam| format!(", {}: {}", p.name, guest_param_type_name(&p.ty)))
.collect::<Vec<_>>()
.join("")
};
let ret_type: String = match &func.returns {
Some(ty) => guest_host_return_type_name(ty),
None => "()".to_owned(),
};
out.push_str(&format!(
" /// Call host contract function `{}` (function_id={})\n",
func.name, fn_id
));
let self_param: &str = if needs_arena { "&mut self" } else { "&self" };
if needs_arena {
out.push_str(
" /// Returns a value borrowing this caller's arena; it stays valid until\n\
\x20 /// the next arena-backed call on this caller.\n",
);
}
out.push_str(&format!(
" pub fn {}({self_param}{}) -> Result<{ret_type}, HostContractError> {{\n",
func.name, params_str
));
if needs_arena {
out.push_str(
" // Reset the arena at call start: frees the previous call's overflow\n",
);
out.push_str(
" // blocks and rewinds the primary region, invalidating prior views.\n",
);
out.push_str(" self.arena.reset();\n");
}
out.push_str(" if self.interface.is_null() {\n");
out.push_str(
" return Err(HostContractError::new(AbiErrorCode::HostContractNotFound));\n",
);
out.push_str(" }\n");
out.push_str(
" // SAFETY: self.interface is non-null (checked above) and points to a valid\n",
);
out.push_str(" // HostContractInterface produced by resolve_host_contract_interface.\n");
out.push_str(
" let interface: &HostContractInterface = unsafe { &*self.interface };\n\n",
);
emit_guest_host_contract_args_setup(out, func, caller_name);
emit_guest_host_contract_out_setup(out, &func.returns);
out.push_str(" let mut err: AbiError = AbiError::ok();\n");
out.push_str(
" // SAFETY: args_ptr/out_ptr/&mut err match the host-contract ABI contract.\n",
);
out.push_str(" unsafe {\n");
out.push_str(" match interface.dispatch_type {\n");
out.push_str(" DispatchType::Native => {\n");
out.push_str(&format!(
" if interface.dispatch.native.function_count < {fn_id}_u32 + 1 {{\n"
));
out.push_str(
" return Err(HostContractError::new(AbiErrorCode::HostContractCallFailed));\n",
);
out.push_str(" }\n");
out.push_str(&format!(
" let fn_ptr: *const () = *interface.dispatch.native.functions.add({fn_id}_usize);\n"
));
out.push_str(" // SAFETY: Transmuting *const () to a function pointer is sound because:\n");
out.push_str(" // - Function pointers have the same size and alignment as data pointers\n");
out.push_str(" // - The interface guarantees that the function at this index is a native dispatch\n");
out.push_str(" // with the exact signature: unsafe extern \"C\" fn(*const (), *const (), *mut (), *mut AbiError)\n");
out.push_str(" let dispatch_fn: unsafe extern \"C\" fn(*const (), *const (), *mut (), *mut AbiError) = core::mem::transmute(fn_ptr);\n");
out.push_str(
" // The native thunk receives the per-instance state as its first\n",
);
out.push_str(
" // argument and dereferences it as the implementation pointer.\n",
);
out.push_str(
" dispatch_fn(self.instance.data as *const (), args_ptr, out_ptr, &mut err);\n",
);
out.push_str(" }\n");
out.push_str(" DispatchType::VirtualMachine => {\n");
let arena_arg: &str = if needs_arena {
"&mut self.arena as *mut CallArena"
} else {
"core::ptr::null_mut()"
};
out.push_str(&format!(
" (interface.dispatch.vm.call)(interface.dispatch.vm.loader_data, GuestContractInstance::null(), {fn_id}_u32, args_ptr, out_ptr, {arena_arg}, &mut err);\n"
));
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" };\n\n");
out.push_str(" if err.code != AbiErrorCode::Ok as u32 {\n");
out.push_str(" let message: String = if err.message.ptr.is_null() || err.message.len == 0 {\n");
out.push_str(" String::new()\n");
out.push_str(" } else {\n");
out.push_str(" // SAFETY: err.message.ptr is valid for err.message.len bytes and points to UTF-8 data.\n");
out.push_str(
" // The message is owned by the producer (static or runtime-owned); the\n",
);
out.push_str(
" // receiver must NEVER free it. We only copy it into an owned String.\n",
);
out.push_str(" let s: String = unsafe {\n");
out.push_str(" let slice: &[u8] = core::slice::from_raw_parts(err.message.ptr, err.message.len);\n");
out.push_str(" core::str::from_utf8_unchecked(slice).to_owned()\n");
out.push_str(" };\n");
out.push_str(" s\n");
out.push_str(" };\n");
out.push_str(" return Err(HostContractError { code: AbiErrorCode::from_u32(err.code), message });\n");
out.push_str(" }\n\n");
emit_out_assume_init(out, &func.returns);
match &func.returns {
Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)) => {
out.push_str(
" // SAFETY: out_val.ptr points to host-owned memory that the contract guarantees\n\
\x20 // stays valid until the next arena-backed call on this caller. The returned &str\n\
\x20 // borrows &mut self, so the borrow checker forbids another call (which would reset\n\
\x20 // the arena and invalidate this memory) while the view is alive. A null/empty view\n\
\x20 // never dereferences the pointer; non-empty bytes are valid UTF-8 per TRUST_MODEL.\n\
\x20 let result: &str = if out_val.ptr.is_null() || out_val.len == 0 {\n\
\x20 \"\"\n\
\x20 } else {\n\
\x20 unsafe {\n\
\x20 let slice: &[u8] = core::slice::from_raw_parts(out_val.ptr, out_val.len);\n\
\x20 core::str::from_utf8_unchecked(slice)\n\
\x20 }\n\
\x20 };\n\
\x20 Ok(result)\n",
);
}
Some(ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)) => {
out.push_str(
" // SAFETY: out_val.ptr points to host-owned memory valid until the next arena-backed\n\
\x20 // call on this caller; the returned &[u8] borrows &mut self, so no second call can\n\
\x20 // invalidate it while the view is alive. A null/empty buffer never dereferences.\n\
\x20 let result: &[u8] = if out_val.ptr.is_null() || out_val.len == 0 {\n\
\x20 &[]\n\
\x20 } else {\n\
\x20 unsafe { core::slice::from_raw_parts(out_val.ptr, out_val.len) }\n\
\x20 };\n\
\x20 Ok(result)\n",
);
}
Some(_) => {
out.push_str(" Ok(out_val)\n");
}
None => {
out.push_str(" Ok(())\n");
}
}
out.push_str(" }\n\n");
}
fn guest_param_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.rust_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "String".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Vec<u8>".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "*mut ()".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "()".to_owned(),
ResolvedTypeRef::UserDefined(name) => name.clone(),
}
}
fn guest_host_return_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "&str".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "&[u8]".to_owned(),
other => rust_type_name(other),
}
}
fn emit_guest_host_contract_args_setup(
out: &mut String,
func: &ResolvedFunction,
caller_name: &str,
) {
if func.params.is_empty() {
out.push_str(" let args_ptr: *const () = core::ptr::null();\n");
return;
}
let any_string_view: bool = func
.params
.iter()
.any(|p: &ResolvedParam| matches!(p.ty, ResolvedTypeRef::AbiType(AbiBuiltin::StringView)));
if any_string_view {
out.push_str(
" // SAFETY: self.host was captured from a valid HostApi in `from_host`\n\
\x20 // and stays valid for the runtime's lifetime.\n\
\x20 let host_ctx: HostContext = unsafe { HostContext::new(self.host) };\n",
);
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
let ty_name: String = guest_param_type_name(¶m.ty);
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" let {name}_view: StringView = host_ctx.alloc_string(&{name}).unwrap_or_else(|_| string_view_null());\n",
name = param.name
));
out.push_str(&format!(
" let args_ptr: *const () = &{name}_view as *const StringView as *const ();\n",
name = param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" let {name}_buf: Buffer = Buffer {{ ptr: {name}.as_mut_ptr(), len: {name}.len(), cap: {name}.capacity() }};\n",
name = param.name
));
out.push_str(&format!(
" let args_ptr: *const () = &{name}_buf as *const Buffer as *const ();\n",
name = param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" let args_ptr: *const () = &{name} as *const {ty} as *const ();\n",
name = param.name,
ty = ty_name
));
}
_ => {
out.push_str(&format!(
" let {name}_val: {ty} = {name};\n",
name = param.name,
ty = ty_name
));
out.push_str(&format!(
" let args_ptr: *const () = &{name}_val as *const {ty} as *const ();\n",
name = param.name,
ty = ty_name
));
}
}
return;
}
let trait_name: &str = caller_name.strip_suffix("Caller").unwrap_or(caller_name);
let pack_struct: String = arg_pack_struct_name(trait_name, &func.name);
out.push_str(&format!(
" let args_val: {pack_struct} = {pack_struct} {{\n"
));
for param in &func.params {
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" {}: host_ctx.alloc_string(&{}).unwrap_or_else(|_| string_view_null()),\n",
param.name, param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" {}: Buffer {{ ptr: {}.as_mut_ptr(), len: {}.len(), cap: {}.capacity() }},\n",
param.name, param.name, param.name, param.name
));
}
_ => {
out.push_str(&format!(" {},\n", param.name));
}
}
}
out.push_str(" };\n");
out.push_str(&format!(
" let args_ptr: *const () = &args_val as *const {pack_struct} as *const ();\n"
));
}
fn emit_guest_host_contract_out_setup(out: &mut String, returns: &Option<ResolvedTypeRef>) {
if let Some(ret_ty) = returns {
let ty_name: String = rust_type_name(ret_ty);
out.push_str(&format!(
" let mut out_val: core::mem::MaybeUninit<{ty_name}> = core::mem::MaybeUninit::uninit();\n"
));
out.push_str(" let out_ptr: *mut () = out_val.as_mut_ptr() as *mut ();\n");
} else {
out.push_str(" let out_ptr: *mut () = core::ptr::null_mut();\n");
}
}
const _: fn() = || {
let _: String = rust_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U8));
};
fn generate_peer_callers_file(ir: &ValidatedIr, peers: &[&ResolvedContract]) -> String {
let header: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
// Re-generate with: polyplugc generate --bundle bundle.toml --lang rust --out <dir>\n\
#![allow(unused_imports)]\n\
#![allow(dead_code)]\n#![allow(non_snake_case)]\n#![allow(clippy::eq_op)]\n#![allow(clippy::identity_op)]\n\n";
let mut out: String = String::new();
out.push_str(header);
out.push_str("use polyplug_abi::HostApi;\n");
out.push_str("use polyplug_abi::GuestContractHandle;\n");
out.push_str("use polyplug_abi::GuestContractInterface;\n");
out.push_str("use polyplug_abi::GuestContractInstance;\n");
out.push_str("use polyplug_abi::StringView;\n");
out.push_str("use polyplug_abi::AbiError;\n");
out.push_str("use polyplug_abi::AbiErrorCode;\n");
out.push_str("use polyplug_abi::DispatchType;\n");
let any_needs_arena: bool = peers
.iter()
.any(|c: &&ResolvedContract| contract_needs_arena(c));
if any_needs_arena {
out.push_str("use polyplug_abi::CallArena;\n");
}
out.push_str("use super::types::*;\n\n");
if any_needs_arena {
out.push_str("const CALL_ARENA_BUF_LEN: usize = 512;\n\n");
}
out.push_str("/// Error returned from peer guest contract calls.\n");
out.push_str("#[derive(Debug)]\n");
out.push_str("pub struct PeerCallError {\n");
out.push_str(" /// ABI error code (non-zero).\n");
out.push_str(" pub code: AbiErrorCode,\n");
out.push_str(" /// Human-readable error message (may be empty).\n");
out.push_str(" pub message: String,\n");
out.push_str("}\n\n");
out.push_str("impl PeerCallError {\n");
out.push_str(" /// Create a new error with the given code and no message.\n");
out.push_str(" pub fn new(code: AbiErrorCode) -> Self {\n");
out.push_str(" Self { code, message: String::new() }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
for contract in peers {
let min_ver: u32 = peer_min_version(ir, contract.contract_id);
generate_peer_caller(&mut out, contract, min_ver);
}
out
}
fn generate_peer_caller(out: &mut String, contract: &ResolvedContract, min_version: u32) {
let struct_name: String = format!("{}Peer", contract_name_to_struct(&contract.name));
let needs_arena: bool = contract_needs_arena(contract);
out.push_str(&format!(
"/// Peer caller for guest contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str("///\n");
out.push_str(
"/// Dispatches directly through the cached peer interface — the same near-bare-metal\n\
/// path as the host→guest caller (no per-call registry resolve, no epoch pin).\n\
/// The declared dependency keeps the\n\
/// peer alive (its unload is refused while we are loaded); a hot-reload is caught by\n\
/// the cached revision counter, which re-resolves before the next dispatch.\n",
);
if needs_arena {
out.push_str("///\n");
out.push_str(
"/// # Call-arena lifetime\n\
///\n\
/// Methods returning variable-size values (`StringView`, `Buffer`, or structs\n\
/// that may embed one) take `&mut self` and reset this caller's arena at the\n\
/// start of the call. Any view returned by such a method borrows arena memory\n\
/// and is valid only until the next arena-backed call on the same caller.\n",
);
}
out.push_str(&format!("pub struct {struct_name} {{\n"));
out.push_str(" /// Resolved interface pointer for the peer contract.\n");
out.push_str(" interface: *const GuestContractInterface,\n");
out.push_str(" /// Instance handle created from the peer interface.\n");
out.push_str(" /// A null `instance.data` is valid for stateless contracts.\n");
out.push_str(" instance: GuestContractInstance,\n");
out.push_str(
" /// Host interface pointer used to resolve the peer and for instance lifecycle\n\
\x20 /// (create/destroy) — NOT for dispatch, which goes straight through `interface`.\n",
);
out.push_str(" host: *const HostApi,\n");
out.push_str(
" /// Peer contract handle, retained so the cache can re-resolve after a hot-reload\n",
);
out.push_str(
" /// (which swaps a new interface into the same slot) or report a gone contract.\n",
);
out.push_str(" handle: GuestContractHandle,\n");
out.push_str(" /// Pointer to the runtime's registry revision counter, fetched once via\n");
out.push_str(" /// `HostApi.revision_counter`. Polled directly before each dispatch (one\n");
out.push_str(
" /// atomic load, no call into the runtime); null when there is no runtime.\n",
);
out.push_str(" revision_ptr: *const u64,\n");
out.push_str(
" /// Revision value read when the peer was resolved. Compared before each dispatch\n",
);
out.push_str(
" /// against the live counter to detect a reload/unload and re-resolve, so the cached\n",
);
out.push_str(" /// interface pointer and instance never dangle.\n");
out.push_str(" cached_revision: u64,\n");
if needs_arena {
out.push_str(
" /// Stable-address backing buffer for the per-call arena. Boxed so the\n\
\x20 /// arena's interior pointers stay valid when the caller is moved.\n",
);
out.push_str(" arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]>,\n");
out.push_str(
" /// Per-call bump arena over `arena_buf`, reset at each arena-backed call.\n",
);
out.push_str(" arena: CallArena,\n");
}
out.push_str("}\n\n");
out.push_str(&format!("impl {struct_name} {{\n"));
out.push_str(" /// Discover and resolve the peer contract through the host.\n");
out.push_str(" ///\n");
out.push_str(" /// `host` is the per-instance context handed to the author factory\n");
out.push_str(" /// (`polyplug_create_<plugin>`) — no process-wide host storage exists.\n");
out.push_str(" ///\n");
out.push_str(
" /// Returns `None` if the host context is null, the contract is not found,\n",
);
out.push_str(" /// or the resolved interface pointer is null.\n");
out.push_str(" pub fn resolve(host: polyplug_guest::HostContext) -> Option<Self> {\n");
out.push_str(" let host: *const HostApi = host.as_ptr();\n");
out.push_str(
" // SAFETY: host is checked for null via as_ref() which returns None on null.\n",
);
out.push_str(" let iface_api: &HostApi = unsafe { host.as_ref()? };\n");
out.push_str(" // SAFETY: iface_api is the reborrowed non-null HostApi; find_guest_contract is an\n");
out.push_str(" // ABI function pointer safe to call with a valid host (returns a zero handle if absent).\n");
out.push_str(&format!(
" let handle: GuestContractHandle = unsafe {{\n\
\x20 (iface_api.find_guest_contract)(host, 0x{:016X}_u64, {min_version}_u32)\n\
\x20 }};\n",
contract.contract_id
));
out.push_str(" // SAFETY: handle is the value returned by find_guest_contract; resolve_guest_contract\n");
out.push_str(
" // is safe to call with any handle (returns null for stale/invalid handles).\n",
);
out.push_str(" let interface: *const GuestContractInterface = unsafe {\n");
out.push_str(" (iface_api.resolve_guest_contract)(host, handle)\n");
out.push_str(" };\n");
out.push_str(" if interface.is_null() {\n");
out.push_str(" return None;\n");
out.push_str(" }\n");
out.push_str(" // A null instance.data is valid: stateless contracts return a null\n");
out.push_str(
" // handle from create_instance and use it as an opaque dispatch token.\n",
);
out.push_str(
" // Route creation through the host so the runtime tracks the instance.\n",
);
out.push_str(
" let mut instance: GuestContractInstance = GuestContractInstance::null();\n",
);
out.push_str(
" // SAFETY: interface is non-null (checked above) and points to a valid\n",
);
out.push_str(" // GuestContractInterface produced by resolve_guest_contract.\n");
out.push_str(" unsafe {\n");
out.push_str(
" (iface_api.create_guest_instance)(host, interface, core::ptr::null(), &mut instance);\n",
);
out.push_str(" };\n");
out.push_str(
" // Fetch the registry revision counter ONCE, then read its current value, so\n",
);
out.push_str(
" // every later call can detect a reload/unload with a direct atomic load (no\n",
);
out.push_str(" // call back into the runtime) and re-resolve before dispatching.\n");
out.push_str(
" // SAFETY: iface_api is the reborrowed non-null HostApi; revision_counter is an\n",
);
out.push_str(" // ABI function pointer safe to call with a valid host.\n");
out.push_str(
" let revision_ptr: *const u64 = unsafe { (iface_api.revision_counter)(host) };\n",
);
out.push_str(" let cached_revision: u64 = if revision_ptr.is_null() {\n");
out.push_str(" 0\n");
out.push_str(" } else {\n");
out.push_str(
" // SAFETY: revision_ptr was returned by revision_counter and points to the\n",
);
out.push_str(" // runtime's revision counter (an AtomicU64), valid for the runtime's lifetime.\n");
out.push_str(" unsafe {\n");
out.push_str(" (*(revision_ptr as *const core::sync::atomic::AtomicU64))\n");
out.push_str(" .load(core::sync::atomic::Ordering::Acquire)\n");
out.push_str(" }\n");
out.push_str(" };\n");
if needs_arena {
out.push_str(
" // Box the backing buffer first so the arena's interior pointers refer\n",
);
out.push_str(
" // to a stable heap address that survives moving the caller value.\n",
);
out.push_str(" let mut arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]> = Box::new([0u8; CALL_ARENA_BUF_LEN]);\n");
out.push_str(
" let arena: CallArena = CallArena::new(arena_buf.as_mut_slice(), host);\n",
);
out.push_str(&format!(
" Some({struct_name} {{ interface, instance, host, handle, revision_ptr, cached_revision, arena_buf, arena }})\n"
));
} else {
out.push_str(&format!(
" Some({struct_name} {{ interface, instance, host, handle, revision_ptr, cached_revision }})\n"
));
}
out.push_str(" }\n\n");
out.push_str(
" /// Read the registry revision through the cached pointer — one aligned atomic\n",
);
out.push_str(
" /// load, no call into the runtime. Returns the cached value (\"unchanged\") when\n",
);
out.push_str(" /// there is no counter (null host/runtime), making the check a no-op.\n");
out.push_str(" #[inline]\n");
out.push_str(" fn live_revision(&self) -> u64 {\n");
out.push_str(" if self.revision_ptr.is_null() {\n");
out.push_str(" return self.cached_revision;\n");
out.push_str(" }\n");
out.push_str(
" // SAFETY: revision_ptr was returned by HostApi.revision_counter and points to\n",
);
out.push_str(" // the runtime's revision counter — an AtomicU64 whose address is stable for the\n");
out.push_str(" // runtime's lifetime, i.e. for as long as this caller can dispatch.\n");
out.push_str(" unsafe {\n");
out.push_str(" (*(self.revision_ptr as *const core::sync::atomic::AtomicU64))\n");
out.push_str(" .load(core::sync::atomic::Ordering::Acquire)\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" /// Returns `true` if this peer holds a resolved (non-null) interface.\n");
out.push_str(" pub fn is_valid(&self) -> bool {\n");
out.push_str(" !self.interface.is_null()\n");
out.push_str(" }\n\n");
out.push_str(
" /// Re-resolve the cached peer interface after the registry changed under us.\n",
);
out.push_str(" ///\n");
out.push_str(" /// Invoked automatically when a dispatch observes a revision change. A\n");
out.push_str(
" /// 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,\n",
);
out.push_str(" /// so it resolves to null and `false` is returned (the peer is gone).\n");
out.push_str(" ///\n");
out.push_str(" /// The old instance is ABANDONED, never destroyed: after a reload its\n");
out.push_str(
" /// interface — and the guest-side state it created — is already epoch-reclaimed,\n",
);
out.push_str(" /// so calling the dead interface's `destroy_instance` would be undefined\n");
out.push_str(
" /// behaviour. The runtime reclaims the old instance's backing as part of the\n",
);
out.push_str(" /// reload, then a fresh instance is created on the new interface.\n");
out.push_str(" fn revalidate(&mut self) -> bool {\n");
out.push_str(" // SAFETY: self.host is the stored host pointer; as_ref() returns None when it is null.\n");
out.push_str(" let iface_api: &HostApi = match unsafe { self.host.as_ref() } {\n");
out.push_str(" Some(api) => api,\n");
out.push_str(" None => return false,\n");
out.push_str(" };\n");
out.push_str(" // SAFETY: resolve_guest_contract is an ABI fn ptr safe to call with a valid host and handle.\n");
out.push_str(" let interface: *const GuestContractInterface =\n");
out.push_str(
" unsafe { (iface_api.resolve_guest_contract)(self.host, self.handle) };\n",
);
out.push_str(" if interface.is_null() {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(
" let mut instance: GuestContractInstance = GuestContractInstance::null();\n",
);
out.push_str(" // SAFETY: interface is freshly resolved and valid; create_guest_instance writes the new instance.\n");
out.push_str(" unsafe {\n");
out.push_str(
" (iface_api.create_guest_instance)(self.host, interface, core::ptr::null(), &mut instance);\n",
);
out.push_str(" }\n");
out.push_str(" self.instance = instance;\n");
out.push_str(" self.interface = interface;\n");
out.push_str(" self.cached_revision = self.live_revision();\n");
out.push_str(" true\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_peer_fn_caller(out, func, &struct_name);
}
out.push_str("}\n\n");
out.push_str(&format!("impl Drop for {struct_name} {{\n"));
out.push_str(" fn drop(&mut self) {\n");
if needs_arena {
out.push_str(
" // Free any overflow blocks the arena still holds before dropping.\n",
);
out.push_str(" self.arena.reset();\n");
}
out.push_str(
" // Route destruction through the host so the runtime drops the instance from\n",
);
out.push_str(
" // its live-instance accounting. A null host pointer means there is no\n",
);
out.push_str(" // runtime to mediate the lifecycle, so skip the destroy.\n");
out.push_str(" // SAFETY: self.host is the stored host pointer; as_ref() returns None when it is\n");
out.push_str(" // null, so the borrow only occurs for a valid HostApi.\n");
out.push_str(" let host_api: &HostApi = match unsafe { self.host.as_ref() } {\n");
out.push_str(" Some(api) => api,\n");
out.push_str(" None => return,\n");
out.push_str(" };\n");
out.push_str(
" // If the registry changed since we resolved, the cached interface and instance\n",
);
out.push_str(
" // are stale — a reload/unload reclaimed their backing — so calling the dead\n",
);
out.push_str(
" // interface's destroy would be UB; the reload/unload already reclaimed the\n",
);
out.push_str(" // instance, so skip the destroy entirely.\n");
out.push_str(" if self.live_revision() != self.cached_revision {\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" // We guard on instance.data to skip the call for stateless (null-data) contracts.\n");
out.push_str(" if !self.instance.data.is_null() {\n");
out.push_str(
" // SAFETY: instance was created by create_guest_instance on the resolved interface.\n",
);
out.push_str(" // The interface pointer is stored for the lifetime of this wrapper and is valid.\n");
out.push_str(" unsafe {\n");
out.push_str(
" (host_api.destroy_guest_instance)(self.host, self.interface, self.instance);\n",
);
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
fn generate_peer_fn_caller(out: &mut String, func: &ResolvedFunction, struct_name: &str) {
let fn_id: u32 = func.function_id;
let needs_arena: bool = fn_needs_arena(func);
let sig_params: String = build_sig_params(func);
let ret_type: String = match &func.returns {
Some(ty) => rust_type_name(ty),
None => "()".to_owned(),
};
out.push_str(&format!(
" /// Call peer function `{}` (function_id={}) via direct cached-interface dispatch.\n",
func.name, fn_id
));
out.push_str(" #[allow(clippy::absurd_extreme_comparisons)]\n");
let self_param: &str = "&mut self";
if needs_arena {
out.push_str(
" /// Returns a value borrowing this caller's arena; it stays valid until\n\
\x20 /// the next arena-backed call on this caller.\n",
);
}
out.push_str(&format!(
" pub fn {}({self_param}{sig_params}) -> Result<{ret_type}, PeerCallError> {{\n",
func.name
));
out.push_str(
" // Cheap per-call staleness check: read the registry revision directly through\n",
);
out.push_str(" // the cached pointer (one atomic load, no call into the runtime). On any change\n");
out.push_str(" // (hot-reload or unload of the peer) we re-resolve first, so the cached interface\n");
out.push_str(" // and instance are never used once they dangle.\n");
out.push_str(
" if self.live_revision() != self.cached_revision && !self.revalidate() {\n",
);
out.push_str(" return Err(PeerCallError::new(AbiErrorCode::NotFound));\n");
out.push_str(" }\n");
if needs_arena {
out.push_str(
" // Reset the arena at call start: frees the previous call's overflow\n",
);
out.push_str(
" // blocks and rewinds the primary region, invalidating prior views.\n",
);
out.push_str(" self.arena.reset();\n");
}
emit_args_setup(out, func, struct_name);
emit_out_setup(out, &func.returns);
let param_ty_comment: String = args_type_comment(func, struct_name);
let ret_ty_comment: String = ret_type.clone();
out.push_str(&format!(
" // SAFETY: args_ptr points to a valid {param_ty_comment} and out_ptr to a valid {ret_ty_comment}.\n"
));
out.push_str(" // Enforced by the generated caller contract.\n");
out.push_str(" let out_ptr: *mut () = ");
if func.returns.is_some() {
out.push_str("out_val.as_mut_ptr() as *mut ();\n");
} else {
out.push_str("core::ptr::null_mut();\n");
}
emit_native_vm_dispatch(out, fn_id, needs_arena);
out.push_str(" if err.code != AbiErrorCode::Ok as u32 {\n");
out.push_str(" let message: String = if err.message.ptr.is_null() || err.message.len == 0 {\n");
out.push_str(" String::new()\n");
out.push_str(" } else {\n");
out.push_str(" // SAFETY: err.message.ptr is valid for err.message.len bytes and points to UTF-8 data.\n");
out.push_str(
" // The message is owned by the producer (static or runtime-owned); the\n",
);
out.push_str(
" // receiver must NEVER free it. We only copy it into an owned String.\n",
);
out.push_str(" let s: String = unsafe {\n");
out.push_str(" let slice: &[u8] = core::slice::from_raw_parts(err.message.ptr, err.message.len);\n");
out.push_str(" core::str::from_utf8_unchecked(slice).to_owned()\n");
out.push_str(" };\n");
out.push_str(" s\n");
out.push_str(" };\n");
out.push_str(
" return Err(PeerCallError { code: AbiErrorCode::from_u32(err.code), message });\n",
);
out.push_str(" }\n");
if func.returns.is_some() {
emit_out_assume_init(out, &func.returns);
out.push_str(" Ok(out_val)\n");
} else {
out.push_str(" Ok(())\n");
}
out.push_str(" }\n\n");
}
fn generate_guest_mod_rs(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(RUST_FILE_HEADER);
out.push_str("pub mod contracts;\n");
out.push_str("pub mod init;\n");
out.push_str("pub mod types;\n");
out.push_str("pub mod interfaces;\n");
if !ir.host_contracts.is_empty() {
out.push_str("pub mod host_contract_callers;\n");
}
let peer_contracts: Vec<&ResolvedContract> = collect_peer_contracts(ir);
if !peer_contracts.is_empty() {
out.push_str("pub mod peer_callers;\n");
}
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use crate::ir::ReprType;
use crate::ir::Version;
#[test]
fn contract_name_to_struct_conversion() {
assert_eq!(
contract_name_to_struct("image.decode"),
"ImageDecodeContract"
);
assert_eq!(contract_name_to_struct("audio"), "AudioContract");
}
#[test]
fn generate_host_empty_ir() {
let generator: RustGenerator = RustGenerator;
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");
assert_eq!(files.files.len(), 5); assert!(files.files[0].content.contains("AUTO-GENERATED"));
assert!(files.files[1].content.contains("AUTO-GENERATED"));
assert!(files.files[2].content.contains("AUTO-GENERATED"));
}
#[test]
fn generate_host_produces_three_files() {
let generator: RustGenerator = RustGenerator;
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/types.rs".to_owned()));
assert!(names.contains(&"host/host_callers.rs".to_owned()));
assert!(names.contains(&"manifest.toml".to_owned()));
}
#[test]
fn arg_pack_struct_name_conversion() {
assert_eq!(
arg_pack_struct_name("TestAddContract", "add_primitive"),
"TestAddContractAddPrimitiveArgs"
);
assert_eq!(
arg_pack_struct_name("TestAddContract", "reset"),
"TestAddContractResetArgs"
);
}
#[test]
fn generate_rust_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 mut out: String = String::new();
generate_rust_enum(&mut out, &e);
assert!(out.contains("#[repr(u32)]"), "missing repr attr: {out}");
assert!(
out.contains("pub enum PixelFormat"),
"missing enum def: {out}"
);
assert!(
out.contains("Unknown = 0"),
"missing Unknown variant: {out}"
);
}
#[test]
fn generate_rust_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 mut out: String = String::new();
generate_rust_enum(&mut out, &e);
assert!(out.contains("pub mod image_flags"), "missing module: {out}");
assert!(
out.contains("pub type ImageFlags = u32"),
"missing type alias: {out}"
);
assert!(out.contains("pub const NONE"), "missing NONE const: {out}");
assert!(
out.contains("pub const COMPRESSED"),
"missing COMPRESSED const: {out}"
);
}
#[test]
fn generate_rust_enum_variant_ref_substitution() {
let e: EnumDef = EnumDef {
name: "Flags".to_owned(),
repr: ReprType::U32,
bitflag: false,
variants: vec![
EnumVariant {
name: "Compressed".to_owned(),
value: "1".to_owned(),
},
EnumVariant {
name: "Hdr".to_owned(),
value: "1 << 1".to_owned(),
},
EnumVariant {
name: "CompressedHdr".to_owned(),
value: "Compressed | Hdr".to_owned(),
},
],
};
let mut out: String = String::new();
generate_rust_enum(&mut out, &e);
assert!(
out.contains("Self::Compressed as u32"),
"missing Self::Compressed cast: {out}"
);
assert!(
out.contains("Self::Hdr as u32"),
"missing Self::Hdr cast: {out}"
);
}
#[test]
fn host_contract_name_to_trait_conversion() {
assert_eq!(host_contract_name_to_trait("host.logger"), "HostLogger");
assert_eq!(
host_contract_name_to_trait("host.fs.reader"),
"HostFsReader"
);
assert_eq!(host_contract_name_to_trait("host.HostLogger"), "HostLogger");
assert_eq!(host_contract_name_to_trait("logger"), "HostLogger");
}
#[test]
fn host_param_type_name_mappings() {
assert_eq!(
host_param_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"u32"
);
assert_eq!(
host_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"&str"
);
assert_eq!(
host_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"&[u8]"
);
assert_eq!(
host_param_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"&MyStruct"
);
}
#[test]
fn host_return_type_name_mappings() {
assert_eq!(
host_return_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"u32"
);
assert_eq!(
host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"String"
);
assert_eq!(
host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"Vec<u8>"
);
assert_eq!(
host_return_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"MyStruct"
);
}
#[test]
fn host_caller_with_stringview_return_uses_arena() {
let contract: ResolvedContract = ResolvedContract {
name: "pipeline.decoder".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "decode".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "input".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
};
let mut out: String = String::new();
generate_host_contract_caller(&mut out, &contract).expect("generate caller");
assert!(
out.contains("arena_buf: Box<[u8; CALL_ARENA_BUF_LEN]>"),
"StringView-returning caller must own a boxed arena buffer: {out}"
);
assert!(
out.contains("arena: CallArena"),
"StringView-returning caller must own a CallArena: {out}"
);
assert!(
out.contains("pub fn decode(&mut self"),
"arena-backed method must take &mut self: {out}"
);
assert!(
out.contains("self.arena.reset();"),
"arena-backed method must reset the arena at call start: {out}"
);
assert!(
out.contains("&mut self.arena as *mut CallArena,"),
"arena-backed VM call must pass the per-caller arena: {out}"
);
assert!(
!out.contains("pub fn decode(&self"),
"arena-backed method must NOT remain &self: {out}"
);
}
#[test]
fn host_caller_with_primitive_return_passes_null_arena() {
let contract: ResolvedContract = ResolvedContract {
name: "math.adder".to_owned(),
contract_id: 0x0FED_CBA9_8765_4321_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "add".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "a".to_owned(),
ty: ResolvedTypeRef::Primitive(PrimitiveType::U32),
}],
returns: Some(ResolvedTypeRef::Primitive(PrimitiveType::U32)),
}],
};
let mut out: String = String::new();
generate_host_contract_caller(&mut out, &contract).expect("generate caller");
assert!(
!out.contains("arena: CallArena"),
"primitive-returning caller must not own an arena: {out}"
);
assert!(
out.contains("pub fn add(&mut self"),
"every method now takes &mut self: the per-call staleness check may call \
revalidate(&mut self) to re-resolve after a reload/unload: {out}"
);
assert!(
out.contains("core::ptr::null_mut(),"),
"non-arena VM call must pass a null arena: {out}"
);
}
#[test]
fn generate_host_contract_trait_produces_trait() {
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_host_contract_trait(&mut out, &contract);
assert!(
out.contains("pub trait HostLogger: Send + Sync"),
"missing trait: {out}"
);
assert!(
out.contains("fn log(&self, message: &str) -> ()"),
"missing log method: {out}"
);
assert!(
out.contains("fn logf(&self, level: u32, format: &str) -> ()"),
"missing logf method: {out}"
);
}
#[test]
fn generate_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_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("pub trait HostLogger: Send + Sync"),
"missing trait: {out}"
);
assert!(
out.contains("HOSTLOGGER_CONTRACT_ID"),
"missing constant: {out}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_host_contracts_file() {
let generator: RustGenerator = RustGenerator;
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/host_contracts.rs".to_owned()),
"missing host_contracts.rs: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_host_contracts_file() {
let generator: RustGenerator = RustGenerator;
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/host_contracts.rs".to_owned()),
"unexpected host_contracts.rs: {names:?}"
);
}
#[test]
fn host_contract_name_to_caller_conversion() {
assert_eq!(
host_contract_name_to_caller("host.logger"),
"HostLoggerCaller"
);
assert_eq!(
host_contract_name_to_caller("host.fs.reader"),
"HostFsReaderCaller"
);
}
#[test]
fn guest_param_type_name_mappings() {
assert_eq!(
guest_param_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"u32"
);
assert_eq!(
guest_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"String"
);
assert_eq!(
guest_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"Vec<u8>"
);
assert_eq!(
guest_param_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"MyStruct"
);
}
#[test]
fn guest_host_return_type_name_mappings() {
assert_eq!(
guest_host_return_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"u32"
);
assert_eq!(
guest_host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"&str"
);
assert_eq!(
guest_host_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"&[u8]"
);
assert_eq!(
guest_host_return_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"MyStruct"
);
}
#[test]
fn guest_host_contract_method_stringview_return_emits_borrowed_str() {
let contract: ResolvedHostContract = ResolvedHostContract {
name: "host.cache".to_owned(),
contract_id: 0xAAAA_BBBB_CCCC_DDDD_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "get".to_owned(),
function_id: 0,
params: vec![],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
};
let mut out: String = String::new();
generate_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains("pub fn get(&mut self) -> Result<&str, HostContractError>"),
"StringView return must emit Result<&str, HostContractError>: {out}"
);
assert!(
out.contains("if out_val.ptr.is_null() || out_val.len == 0"),
"StringView return must guard the null/empty view (UB on null ptr even at len==0): {out}"
);
assert!(
out.contains("core::str::from_utf8_unchecked(slice)"),
"non-empty StringView return must build &str from raw parts: {out}"
);
assert!(
out.contains("// SAFETY:"),
"StringView unsafe block must have a SAFETY comment: {out}"
);
}
#[test]
fn guest_host_contract_method_buffer_return_emits_borrowed_slice() {
let contract: ResolvedHostContract = ResolvedHostContract {
name: "host.store".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "read".to_owned(),
function_id: 0,
params: vec![],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
}],
};
let mut out: String = String::new();
generate_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains("pub fn read(&mut self) -> Result<&[u8], HostContractError>"),
"Buffer return must emit Result<&[u8], HostContractError>: {out}"
);
assert!(
out.contains("if out_val.ptr.is_null() || out_val.len == 0"),
"Buffer return must guard the null/empty buffer (UB on null ptr even at len==0): {out}"
);
assert!(
out.contains("core::slice::from_raw_parts(out_val.ptr, out_val.len)"),
"non-empty Buffer return must build &[u8] from raw parts: {out}"
);
assert!(
out.contains("// SAFETY:"),
"Buffer unsafe block must have a SAFETY comment: {out}"
);
}
#[test]
fn host_thunk_single_param_stringview_uses_null_safe_accessor() {
let func: ResolvedFunction = 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_host_thunk(&mut out, &func, "host.logger", "HostLogger");
assert!(
out.contains("message_sv.as_str()"),
"single StringView param must unpack via null-safe as_str(): {out}"
);
assert!(
!out.contains("from_utf8_unchecked(core::slice::from_raw_parts(message_sv.ptr"),
"single StringView param must NOT build str from raw parts (UB on null): {out}"
);
}
#[test]
fn host_thunk_single_param_buffer_uses_null_safe_accessor() {
let func: ResolvedFunction = ResolvedFunction {
name: "write".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "data".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::Buffer),
}],
returns: None,
};
let mut out: String = String::new();
generate_host_thunk(&mut out, &func, "host.store", "HostStore");
assert!(
out.contains("data_buf.as_slice()"),
"single Buffer param must unpack via null-safe as_slice(): {out}"
);
assert!(
!out.contains("core::slice::from_raw_parts(data_buf.ptr"),
"single Buffer param must NOT build slice from raw parts (UB on null): {out}"
);
}
#[test]
fn host_thunk_packed_params_use_null_safe_accessors() {
let func: ResolvedFunction = ResolvedFunction {
name: "emit".to_owned(),
function_id: 0,
params: vec![
ResolvedParam {
name: "tag".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
},
ResolvedParam {
name: "payload".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::Buffer),
},
],
returns: None,
};
let mut out: String = String::new();
generate_host_thunk(&mut out, &func, "host.bus", "HostBus");
assert!(
out.contains("packed.tag.as_str()"),
"packed StringView param must unpack via null-safe as_str(): {out}"
);
assert!(
out.contains("packed.payload.as_slice()"),
"packed Buffer param must unpack via null-safe as_slice(): {out}"
);
assert!(
!out.contains("from_raw_parts(packed."),
"packed params must NOT build slices from raw parts (UB on null): {out}"
);
}
#[test]
fn generate_guest_host_contract_caller_produces_struct() {
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,
}],
};
let mut out: String = String::new();
generate_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains("pub struct HostLoggerCaller"),
"missing struct: {out}"
);
assert!(
out.contains("instance: HostContractInstance"),
"missing instance field: {out}"
);
assert!(
out.contains("pub unsafe fn from_host"),
"missing from_host method: {out}"
);
assert!(
out.contains("pub fn log(&self, message: String) -> Result<(), HostContractError>"),
"missing log method: {out}"
);
}
#[test]
fn generate_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_guest_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("pub struct HostLoggerCaller"),
"missing struct: {out}"
);
assert!(
out.contains("HOSTLOGGER_CONTRACT_ID"),
"missing constant: {out}"
);
}
#[test]
fn generate_guest_with_host_contracts_produces_host_contract_callers_file() {
let generator: RustGenerator = RustGenerator;
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/host_contract_callers.rs".to_owned()),
"missing host_contract_callers.rs: {names:?}"
);
}
#[test]
fn generate_guest_without_host_contracts_no_host_contract_callers_file() {
let generator: RustGenerator = RustGenerator;
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/host_contract_callers.rs".to_owned()),
"unexpected host_contract_callers.rs: {names:?}"
);
}
#[test]
fn generate_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_host_interface_factories_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains(
"pub fn create_host_logger_interface(implementation: Box<dyn HostLogger>)"
),
"missing native factory: {out}"
);
assert!(
out.contains("pub fn create_host_logger_interface_vm"),
"missing vm factory: {out}"
);
assert!(
out.contains("unsafe extern \"C\" fn host_logger_log_thunk"),
"missing thunk: {out}"
);
assert!(
out.contains("std::panic::catch_unwind"),
"missing panic safety: {out}"
);
assert!(
out.contains("AbiErrorCode::Panic"),
"missing panic error code: {out}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_interface_factories_file() {
let generator: RustGenerator = RustGenerator;
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/interface_factories.rs".to_owned()),
"missing interface_factories.rs: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_interface_factories_file() {
let generator: RustGenerator = RustGenerator;
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/interface_factories.rs".to_owned()),
"unexpected interface_factories.rs: {names:?}"
);
}
#[test]
fn interface_factory_has_safety_comments() {
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_host_interface_factories_file(&ir);
assert!(out.contains("// SAFETY:"), "missing SAFETY comment: {out}");
}
#[test]
fn peer_caller_emitted_for_declared_dependency() {
let contract_a: ResolvedContract = ResolvedContract {
name: "pipeline.encoder".to_owned(),
contract_id: 0xAAAA_BBBB_CCCC_DDDD_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "encode".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "input".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
};
let contract_b: ResolvedContract = ResolvedContract {
name: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
version: Version {
major: 2,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "decode".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "data".to_owned(),
ty: ResolvedTypeRef::Primitive(PrimitiveType::U32),
}],
returns: Some(ResolvedTypeRef::Primitive(PrimitiveType::U32)),
}],
};
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![contract_a, contract_b],
host_contracts: vec![],
bundle: Some(ResolvedBundle {
name: "test.bundle".to_owned(),
version: Version {
major: 1,
minor: 0,
patch: 0,
},
loader: "native".to_owned(),
file: polyplug_codegen::ResolvedBundleFile::Single("test.so".to_owned()),
plugins: vec![ResolvedPlugin {
name: "test_plugin".to_owned(),
implements: vec!["pipeline.encoder".to_owned()],
optional: vec![],
}],
bundle_id: 0xDEAD_BEEF_CAFE_BABE_u64,
dependencies: vec![ResolvedDependency::ByContract {
contract: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
min_version: 2,
}],
needs_reinit_on_dep_reload: false,
}),
};
let generator: RustGenerator = RustGenerator;
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let peer_file: Option<&GeneratedFile> = files
.files
.iter()
.find(|f: &&GeneratedFile| f.path == std::path::Path::new("guest/peer_callers.rs"));
assert!(
peer_file.is_some(),
"guest/peer_callers.rs should be emitted"
);
let content: &str = &peer_file.expect("peer_callers.rs").content;
assert!(
content.contains("struct PipelineDecoderContractPeer"),
"missing PipelineDecoderContractPeer struct: {content}"
);
assert!(
content.contains("interface.dispatch.native.functions")
&& content.contains("(interface.dispatch.vm.call)"),
"peer must dispatch directly through the cached interface: {content}"
);
assert!(
content.contains("fn resolve(host: polyplug_guest::HostContext)"),
"missing resolve(host) method: {content}"
);
assert!(
content.contains("fn decode("),
"missing decode method: {content}"
);
assert!(
content.contains("core::mem::MaybeUninit<u32>"),
"peer caller out-slot must be MaybeUninit, not zeroed: {content}"
);
assert!(
content.contains("out_val.assume_init()"),
"peer caller must assume_init the return value on the success path: {content}"
);
assert!(
!content.contains("core::mem::zeroed"),
"peer caller must never fabricate a return value via core::mem::zeroed: {content}"
);
assert!(
!content.contains("PipelineEncoderContractPeer"),
"PipelineEncoderContractPeer must NOT be emitted — it is not a declared dep: {content}"
);
let mod_file: Option<&GeneratedFile> = files
.files
.iter()
.find(|f: &&GeneratedFile| f.path == std::path::Path::new("guest/mod.rs"));
assert!(mod_file.is_some(), "guest/mod.rs must be emitted");
assert!(
mod_file
.expect("mod.rs")
.content
.contains("pub mod peer_callers;"),
"guest/mod.rs must declare peer_callers: {}",
mod_file.expect("mod.rs").content
);
}
#[test]
fn peer_caller_out_slot_is_maybeuninit_for_enum_return() {
let status_enum: crate::ir::EnumDef = crate::ir::EnumDef {
name: "Status".to_owned(),
repr: crate::ir::ReprType::U32,
bitflag: false,
variants: vec![
crate::ir::EnumVariant {
name: "Active".to_owned(),
value: "1".to_owned(),
},
crate::ir::EnumVariant {
name: "Inactive".to_owned(),
value: "2".to_owned(),
},
],
};
let decoder: ResolvedContract = ResolvedContract {
name: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
version: crate::ir::Version {
major: 2,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "decode".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "data".to_owned(),
ty: ResolvedTypeRef::Primitive(PrimitiveType::U32),
}],
returns: Some(ResolvedTypeRef::UserDefined("Status".to_owned())),
}],
};
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![status_enum],
contracts: vec![decoder],
host_contracts: vec![],
bundle: Some(crate::ir::ResolvedBundle {
name: "test.bundle".to_owned(),
version: crate::ir::Version {
major: 1,
minor: 0,
patch: 0,
},
loader: "native".to_owned(),
file: polyplug_codegen::ResolvedBundleFile::Single("test.so".to_owned()),
plugins: vec![crate::ir::ResolvedPlugin {
name: "test_plugin".to_owned(),
implements: vec![],
optional: vec![],
}],
bundle_id: 0xDEAD_BEEF_CAFE_BABE_u64,
dependencies: vec![crate::ir::ResolvedDependency::ByContract {
contract: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
min_version: 2,
}],
needs_reinit_on_dep_reload: false,
}),
};
let generator: RustGenerator = RustGenerator;
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let content: &str = &files
.files
.iter()
.find(|f: &&GeneratedFile| f.path == std::path::Path::new("guest/peer_callers.rs"))
.expect("peer_callers.rs should be emitted")
.content;
assert!(
content.contains("core::mem::MaybeUninit<Status>"),
"enum-return out-slot must be MaybeUninit<Status>: {content}"
);
assert!(
content.contains("out_val.assume_init()"),
"enum-return caller must assume_init on the success path: {content}"
);
assert!(
!content.contains("core::mem::zeroed"),
"enum-return caller must never fabricate a discriminant via core::mem::zeroed: {content}"
);
}
#[test]
fn no_peer_callers_without_dependencies() {
let ir_no_bundle: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.encoder".to_owned(),
contract_id: 0xAAAA_BBBB_CCCC_DDDD_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
bundle: None,
};
let generator: RustGenerator = RustGenerator;
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir_no_bundle, &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/peer_callers.rs".to_owned()),
"guest/peer_callers.rs must NOT be emitted with no bundle: {names:?}"
);
let mod_content: String = files
.files
.iter()
.find(|f: &&GeneratedFile| f.path == std::path::Path::new("guest/mod.rs"))
.map(|f: &GeneratedFile| f.content.clone())
.unwrap_or_default();
assert!(
!mod_content.contains("pub mod peer_callers;"),
"guest/mod.rs must NOT declare peer_callers with no bundle: {mod_content}"
);
}
#[test]
fn guest_host_contract_single_user_defined_param_passes_address() {
let func: ResolvedFunction = ResolvedFunction {
name: "set_mode".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "mode".to_owned(),
ty: ResolvedTypeRef::UserDefined("PixelFormat".to_owned()),
}],
returns: None,
};
let mut out: String = String::new();
emit_guest_host_contract_args_setup(&mut out, &func, "HostThemeCaller");
assert!(
out.contains("&mode as *const PixelFormat as *const ()"),
"by-value user-defined param must be passed by ADDRESS: {out}"
);
assert!(
!out.contains("= mode as *const"),
"casting a by-value enum/struct to a pointer is invalid Rust (E0606): {out}"
);
}
}