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::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 CppGenerator;
const CPP_FILE_HEADER: &str = "// THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n";
impl CodeGenerator for CppGenerator {
fn generate_host(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
let types_hpp: String = generate_types_hpp(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/types.hpp"),
content: types_hpp,
force_regenerate: false,
});
let host_callers_hpp: String = generate_host_callers_hpp(ir)?;
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/host_callers.hpp"),
content: host_callers_hpp,
force_regenerate: false,
});
let manifest_toml: String = generate_manifest_toml();
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("manifest.toml"),
content: manifest_toml,
force_regenerate: true,
});
if !ir.host_contracts.is_empty() {
let host_contracts_hpp: String = generate_cpp_host_contracts_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/host_contracts.hpp"),
content: host_contracts_hpp,
force_regenerate: false,
});
}
if !ir.host_contracts.is_empty() {
let interface_factories_hpp: String = generate_cpp_host_interface_factories_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("host/interface_factories.hpp"),
content: interface_factories_hpp,
force_regenerate: false,
});
}
Ok(())
}
fn generate_guest(
&self,
ir: &ValidatedIr,
files: &mut GeneratedFiles,
) -> Result<(), PolyplugcError> {
let types_hpp: String = generate_types_hpp(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/types.hpp"),
content: types_hpp,
force_regenerate: false,
});
let contracts_hpp: String = generate_contracts_hpp(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/contracts.hpp"),
content: contracts_hpp,
force_regenerate: false,
});
let interfaces_hpp: String = generate_interfaces_hpp(ir)?;
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/interfaces.hpp"),
content: interfaces_hpp,
force_regenerate: false,
});
let init_hpp: String = generate_init_hpp(ir)?;
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/init.hpp"),
content: init_hpp,
force_regenerate: false,
});
if ir.bundle.is_some() {
let manifest_toml: String = generate_bundle_manifest_cpp(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("manifest.toml"),
content: manifest_toml,
force_regenerate: true,
});
}
if !ir.host_contracts.is_empty() {
let host_contracts_hpp: String = generate_cpp_guest_host_contracts_file(ir);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/host_contracts.hpp"),
content: host_contracts_hpp,
force_regenerate: false,
});
}
let peer_contracts: Vec<&ResolvedContract> = collect_peer_contracts(ir);
if !peer_contracts.is_empty() {
let peer_callers_hpp: String = generate_cpp_peer_callers_file(ir, &peer_contracts);
files.files.push(GeneratedFile {
path: std::path::PathBuf::from("guest/peer_callers.hpp"),
content: peer_callers_hpp,
force_regenerate: false,
});
}
Ok(())
}
}
fn generate_types_hpp(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include \"polyplug/abi.hpp\"\n\n");
out.push_str("namespace polyplug_generated {\n\n");
for contract in &ir.contracts {
let contract_upper: String = contract.name.to_uppercase().replace(['.', '-'], "_");
out.push_str(&format!(
"constexpr uint64_t {}_CONTRACT_ID = 0x{:016X};\n",
contract_upper, contract.contract_id
));
}
out.push('\n');
for e in &ir.enums {
generate_cpp_enum(&mut out, e);
}
for ty in &ir.types {
generate_cpp_type(&mut out, ty);
}
out.push_str("} // namespace polyplug_generated\n");
out
}
fn generate_contracts_hpp(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"types.hpp\"\n");
out.push_str("#include <cstdint>\n\n");
out.push_str("namespace polyplug_plugin {\n\n");
out.push_str("struct RuntimeError { uint32_t code; };\n\n");
for contract in &ir.contracts {
generate_cpp_guest_contract_class(&mut out, contract);
}
out.push_str("} // namespace polyplug_plugin\n");
out
}
fn generate_cpp_guest_contract_class(out: &mut String, contract: &ResolvedContract) {
let class_name: String = contract_name_to_guest_contract_class(&contract.name);
out.push_str(&format!(
"/// Abstract plugin base for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str(&format!("class {} {{\npublic:\n", class_name));
out.push_str(&format!(" virtual ~{}() = default;\n", class_name));
for func in &contract.functions {
generate_cpp_guest_abstract_method(out, func);
}
out.push_str("};\n\n");
}
fn generate_cpp_guest_abstract_method(out: &mut String, func: &ResolvedFunction) {
let return_type: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
let params: Vec<String> = func
.params
.iter()
.map(|p| {
let cpp_ty: String = cpp_type_name(&p.ty);
match &p.ty {
ResolvedTypeRef::UserDefined(_) => format!("const {}& {}", cpp_ty, p.name),
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => {
format!("{} {}", cpp_ty, p.name)
}
}
})
.collect();
let params_str: String = params.join(", ");
out.push_str(&format!(
" virtual {} {}({}) = 0;\n",
return_type, func.name, params_str
));
}
fn generate_interfaces_hpp(ir: &ValidatedIr) -> Result<String, PolyplugcError> {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"contracts.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include <cstring>\n");
out.push_str("#include <exception>\n\n");
out.push_str("namespace polyplug_plugin {\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_cpp_guest_plugin_interface(
&mut out,
&plugin.name,
contract,
is_native_runtime(&bundle.loader),
)?;
}
}
}
} else {
for contract in &ir.contracts {
generate_cpp_guest_contract_interface(&mut out, contract, true)?;
}
}
out.push_str("} // namespace polyplug_plugin\n");
Ok(out)
}
fn generate_cpp_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 class_name: String = contract_name_to_guest_contract_class(&contract.name);
let fn_count: usize = contract.functions.len();
let state_struct: String = format!("{}InstanceState", snake_to_pascal(&plugin_lower));
out.push_str(&format!("// Plugin: {}\n", plugin_name));
out.push_str(&format!(
"constexpr uint64_t {}_CONTRACT_ID = 0x{:016X}ULL;\n\n",
plugin_upper, contract.contract_id
));
emit_cpp_guest_instance_machinery(
out,
&plugin_upper,
&plugin_lower,
&class_name,
&state_struct,
);
for func in &contract.functions {
generate_cpp_guest_abi_wrapper(out, &plugin_lower, &state_struct, func)?;
}
out.push_str(&format!("static void* const {}_FNS[] = {{\n", plugin_upper));
for func in &contract.functions {
out.push_str(&format!(
" reinterpret_cast<void*>({0}_{1}_abi),\n",
plugin_lower, func.name
));
}
out.push_str("};\n\n");
out.push_str(&format!(
"static GuestContractInterface {}_INTERFACE = {{\n",
plugin_upper
));
out.push_str(&format!(" {}_CONTRACT_ID,\n", plugin_upper));
out.push_str(&format!(
" Version{{ {}U, {}U, {}U }}, // contract_version\n",
contract.version.major, contract.version.minor, contract.version.patch
));
let dispatch_type_str: &str = if is_native {
"DispatchType::Native"
} else {
"DispatchType::VirtualMachine"
};
out.push_str(&format!(" {},\n", dispatch_type_str));
out.push_str(&format!(" {}_create_instance,\n", plugin_upper));
out.push_str(&format!(" {}_destroy_instance,\n", plugin_upper));
out.push_str(&format!(
" DispatchMechanisms{{ .native = NativeDispatch{{ {fn_count}U, {}_FNS }} }}\n",
plugin_upper
));
out.push_str("};\n\n");
Ok(())
}
fn generate_cpp_guest_contract_interface(
out: &mut String,
contract: &ResolvedContract,
is_native: bool,
) -> Result<(), PolyplugcError> {
let lower: String = contract_name_to_lower_snake(&contract.name);
let upper: String = contract_name_to_upper_snake(&contract.name);
let class_name: String = contract_name_to_guest_contract_class(&contract.name);
let fn_count: usize = contract.functions.len();
let state_struct: String = format!("{}InstanceState", snake_to_pascal(&lower));
out.push_str(&format!(
"constexpr uint64_t {}_CONTRACT_ID = 0x{:016X}ULL;\n\n",
upper, contract.contract_id
));
emit_cpp_guest_instance_machinery(out, &upper, &lower, &class_name, &state_struct);
for func in &contract.functions {
generate_cpp_guest_abi_wrapper(out, &lower, &state_struct, func)?;
}
out.push_str(&format!("static void* const {}_FNS[] = {{\n", upper));
for func in &contract.functions {
out.push_str(&format!(
" reinterpret_cast<void*>({0}_{1}_abi),\n",
lower, func.name
));
}
out.push_str("};\n\n");
out.push_str(&format!(
"static GuestContractInterface {}_INTERFACE = {{\n",
upper
));
out.push_str(&format!(" {}_CONTRACT_ID,\n", upper));
out.push_str(&format!(
" Version{{ {}U, {}U, {}U }}, // contract_version\n",
contract.version.major, contract.version.minor, contract.version.patch
));
let dispatch_type_str: &str = if is_native {
"DispatchType::Native"
} else {
"DispatchType::VirtualMachine"
};
out.push_str(&format!(" {},\n", dispatch_type_str));
out.push_str(&format!(" {}_create_instance,\n", upper));
out.push_str(&format!(" {}_destroy_instance,\n", upper));
out.push_str(&format!(
" DispatchMechanisms{{ .native = NativeDispatch{{ {}U, {}_FNS }} }}\n",
fn_count, upper
));
out.push_str("};\n\n");
Ok(())
}
fn emit_cpp_guest_instance_machinery(
out: &mut String,
prefix_upper: &str,
lower: &str,
class_name: &str,
state_struct: &str,
) {
out.push_str(&format!(
"// Author-provided factory — implement this in your plugin .cpp. Called once\n\
// per host-created instance; ownership of the returned object transfers to\n\
// the instance (deleted in {prefix_upper}_destroy_instance).\n\
{class_name}* polyplug_create_{lower}(const HostApi* host);\n\n"
));
out.push_str(&format!(
"// Per-instance payload carried in GuestContractInstance.data.\n\
struct {state_struct} {{\n\
\x20 // Host interface captured at instance creation — routes every host call\n\
\x20 // (allocation, logging, peer dispatch) to the runtime that owns it.\n\
\x20 const HostApi* host;\n\
\x20 // The author's implementation, created by polyplug_create_{lower}.\n\
\x20 {class_name}* impl;\n\
}};\n\n"
));
out.push_str(&format!(
"// Create a new instance: calls the author factory and heap-allocates the payload.\n\
// Writes a null handle to *out_instance when host is null, the factory returns\n\
// null, or it throws.\n\
static void {prefix_upper}_create_instance(VmLoaderData loader_data, const HostApi* host, const void* args, GuestContractInstance* out_instance) noexcept {{\n\
\x20 (void)loader_data; // Native-dispatch contracts ignore the VM loader handle.\n\
\x20 (void)args; // Contract-specific init args are unused by generated glue.\n\
\x20 if (out_instance == nullptr) return;\n\
\x20 if (host == nullptr) {{\n\
\x20 *out_instance = GuestContractInstance{{nullptr, 0U}};\n\
\x20 return;\n\
\x20 }}\n\
\x20 try {{\n\
\x20 {class_name}* impl = polyplug_create_{lower}(host);\n\
\x20 if (impl == nullptr) {{\n\
\x20 *out_instance = GuestContractInstance{{nullptr, 0U}};\n\
\x20 return;\n\
\x20 }}\n\
\x20 auto* state = new {state_struct}{{host, impl}};\n\
\x20 *out_instance = GuestContractInstance{{state, {prefix_upper}_CONTRACT_ID}};\n\
\x20 }} catch (...) {{\n\
\x20 *out_instance = GuestContractInstance{{nullptr, 0U}};\n\
\x20 }}\n\
}}\n\n"
));
out.push_str(&format!(
"// Destroy an instance created by {prefix_upper}_create_instance: deletes the\n\
// implementation (ownership transferred from the factory) and the payload.\n\
static void {prefix_upper}_destroy_instance(VmLoaderData loader_data, const HostApi* host, GuestContractInstance instance) noexcept {{\n\
\x20 (void)loader_data; // Native-dispatch contracts ignore the VM loader handle.\n\
\x20 (void)host; // The payload is guest-owned; no host call is needed to free it.\n\
\x20 if (instance.data == nullptr) {{\n\
\x20 return;\n\
\x20 }}\n\
\x20 auto* state = static_cast<{state_struct}*>(instance.data);\n\
\x20 delete state->impl;\n\
\x20 delete state;\n\
}}\n\n"
));
}
fn snake_to_pascal(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_cpp_guest_abi_wrapper(
out: &mut String,
contract_lower: &str,
state_struct: &str,
func: &ResolvedFunction,
) -> Result<(), PolyplugcError> {
let fn_id: u32 = func.function_id;
let is_void_return: bool = matches!(
func.returns.as_ref(),
None | Some(ResolvedTypeRef::AbiType(AbiBuiltin::Void))
);
let has_params: bool = !func.params.is_empty();
out.push_str(&format!(
"// ABI wrapper for {} (function_id = {})\n",
func.name, fn_id
));
out.push_str(&format!(
"inline void {0}_{1}_abi(GuestContractInstance instance, const void* args, void* out, AbiError* out_err) noexcept {{\n",
contract_lower, func.name
));
out.push_str(" if (instance.data == nullptr) {\n");
out.push_str(" static constexpr const char* null_inst_msg = \"instance is null\";\n");
out.push_str(
" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::InvalidPointer), StringView{reinterpret_cast<const uint8_t*>(null_inst_msg), 16}};\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" // SAFETY: instance.data was produced by create_instance and stays valid\n");
out.push_str(" // until destroy_instance; the host never mutates it.\n");
out.push_str(&format!(
" const auto* state = static_cast<const {state_struct}*>(instance.data);\n"
));
out.push_str(" try {\n");
if has_params {
out.push_str(" if (args == nullptr) {\n");
out.push_str(
" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::InvalidPointer), StringView{nullptr, 0}};\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
}
if !is_void_return {
out.push_str(" if (out == nullptr) {\n");
out.push_str(
" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::InvalidPointer), StringView{nullptr, 0}};\n",
);
out.push_str(" return;\n");
out.push_str(" }\n");
}
let call_expr: String = build_guest_call_expr(contract_lower, func);
out.push_str(&call_expr);
if is_void_return {
out.push_str(" // SAFETY: out pointer is not dereferenced for void return per ABI contract.\n");
out.push_str(" (void)out;\n");
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Ok), StringView{nullptr, 0}};\n");
out.push_str(" return;\n");
} else {
let ret_type: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
out.push_str(&format!(
" // SAFETY: out is a valid void* pointing to a {ret_type} per ABI contract.\n"
));
out.push_str(" // The host guarantees proper alignment and size before calling this wrapper.\n");
out.push_str(&format!(
" *static_cast<{}*>(out) = result;\n",
ret_type
));
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Ok), StringView{nullptr, 0}};\n");
out.push_str(" return;\n");
}
out.push_str(" } catch (const std::exception&) {\n");
out.push_str(
" // The AbiError message must outlive this stack frame; the host never frees it.\n",
);
out.push_str(
" // e.what() points into the (about-to-be-destroyed) exception object, so we\n",
);
out.push_str(" // return a static literal instead of a dangling pointer.\n");
out.push_str(
" // SAFETY: err_msg is a static constexpr string literal with known length 26.\n",
);
out.push_str(
" static constexpr const char* err_msg = \"guest threw std::exception\";\n",
);
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Generic), StringView{reinterpret_cast<const uint8_t*>(err_msg), 26}};\n");
out.push_str(" return;\n");
out.push_str(" } catch (...) {\n");
out.push_str(
" // SAFETY: panic_msg is a static constexpr string literal with known length 15.\n",
);
out.push_str(" static constexpr const char* panic_msg = \"plugin panicked\";\n");
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Panic), StringView{reinterpret_cast<const uint8_t*>(panic_msg), 15}};\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str("}\n\n");
Ok(())
}
fn build_guest_call_expr(contract_lower: &str, func: &ResolvedFunction) -> String {
let is_void_return: bool = matches!(
func.returns.as_ref(),
None | Some(ResolvedTypeRef::AbiType(AbiBuiltin::Void))
);
let result_prefix: &str = if is_void_return { "" } else { "auto result = " };
let _ = contract_lower;
if func.params.is_empty() {
return format!(
" // SAFETY: args is null for this function per ABI contract; no dereference needed.\n\
(void)args;\n {}state->impl->{}();\n",
result_prefix, func.name
);
}
if func.params.len() == 1 {
let param: &crate::ir::ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::UserDefined(type_name) => {
let qualified_name: String = qualified_user_type(type_name);
return format!(
" // SAFETY: args is a valid const void* pointing to a {qualified_name} per ABI contract.\n\
// The host guarantees proper alignment and size before calling this wrapper.\n\
{}state->impl->{}(*static_cast<const {}*>(args));\n",
result_prefix, func.name, qualified_name
);
}
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => {
let cpp_ty: String = cpp_type_name(¶m.ty);
return format!(
" // SAFETY: args is a valid const void* pointing to a {cpp_ty} per ABI contract.\n\
// The host guarantees proper alignment and size before calling this wrapper.\n\
{}state->impl->{}(*static_cast<const {}*>(args));\n",
result_prefix, func.name, cpp_ty
);
}
}
}
let func_name_cap: String = capitalise_first(&func.name);
let struct_name: String = format!("{}Args", func_name_cap);
let mut code: String = String::new();
code.push_str(" // SAFETY: args is a valid const void* pointing to a packed struct layout per ABI contract.\n");
code.push_str(" // The host guarantees proper alignment and size matching the struct definition below.\n");
code.push_str(&format!(" struct {} {{", struct_name));
for param in &func.params {
let cpp_ty: String = cpp_type_name(¶m.ty);
code.push_str(&format!(" {} {};", cpp_ty, param.name));
}
code.push_str(" };\n");
code.push_str(&format!(
" const {name}* packed = static_cast<const {name}*>(args);\n",
name = struct_name
));
let call_args: Vec<String> = func
.params
.iter()
.map(|p| format!("packed->{}", p.name))
.collect();
let call_args_str: String = call_args.join(", ");
code.push_str(&format!(
" {}state->impl->{}({});\n",
result_prefix, func.name, call_args_str
));
code
}
fn generate_init_hpp(ir: &ValidatedIr) -> Result<String, PolyplugcError> {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"interfaces.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include \"polyplug/guest.hpp\"\n\n");
out.push_str("extern \"C\" uint32_t polyplug_abi_version() { return 1U; }\n\n");
out.push_str("extern \"C\" AbiError polyplug_init(const HostApi* host, const BundleInitContext* ctx) {\n");
out.push_str(" if (!host || !ctx) {\n");
let init_err_msg: &str = "null parameter in polyplug_init";
out.push_str(&format!(
" static constexpr const char* err_msg = \"{init_err_msg}\";\n",
));
out.push_str(&format!(
" return AbiError{{static_cast<uint32_t>(AbiErrorCode::Generic), StringView{{reinterpret_cast<const uint8_t*>(err_msg), {len}}}}};\n",
len = init_err_msg.len()
));
out.push_str(" }\n\n");
out.push_str(
" // No DSO-global state is stored here. The implementation is constructed per\n",
);
out.push_str(" // instance by create_instance (which calls the author factory\n");
out.push_str(
" // polyplug_create_<plugin> with the HostApi pointer); init only registers\n",
);
out.push_str(" // the 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 plugin_lower: String = plugin.name.to_lowercase().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);
let _ = &plugin_lower;
out.push_str(&format!(" // Register plugin: {}\n", plugin.name));
out.push_str(&format!(
" PluginDescriptor desc_{} = {{\n",
plugin_upper
));
out.push_str(&format!(
" {{ (const uint8_t*)\"{name}\", {len}U }}, // name (StringView)\n",
name = plugin.name,
len = plugin.name.len()
));
out.push_str(&format!(
" {{ (const uint8_t*)\"{name}\", {len}U }}, // contract_name (StringView)\n",
name = contract_name_full,
len = contract_name_full.len()
));
out.push_str(&format!(
" {{ {}U, {}U, 0U }} // version (Version)\n",
version_major, version_minor
));
out.push_str(" };\n");
out.push_str(&format!(
" AbiError err_{upper}{{}};\n host->register_guest_contract(host, &desc_{upper}, &polyplug_plugin::{upper}_INTERFACE, &err_{upper});\n",
upper = plugin_upper
));
out.push_str(&format!(
" if (err_{}.code != static_cast<uint32_t>(AbiErrorCode::Ok)) return err_{};\n\n",
plugin_upper, plugin_upper
));
}
} else {
for contract in &ir.contracts {
generate_init_hpp_register_guest_contract(&mut out, contract)?;
}
}
out.push_str(
" return AbiError{static_cast<uint32_t>(AbiErrorCode::Ok), StringView{nullptr, 0}};\n",
);
out.push_str("}\n\n");
Ok(out)
}
fn generate_init_hpp_register_guest_contract(
out: &mut String,
contract: &ResolvedContract,
) -> Result<(), PolyplugcError> {
let upper: String = contract_name_to_upper_snake(&contract.name);
let name_bytes: usize = contract.name.len();
out.push_str(&format!(" // Register contract: {}\n", contract.name));
out.push_str(" PluginDescriptor desc_");
out.push_str(&upper);
out.push_str(" = {\n");
let contract_name_full: String = format!(
"{}@{}.{}",
contract.name, contract.version.major, contract.version.minor
);
let name_bytes_full: usize = contract_name_full.len();
out.push_str(&format!(
" {{ (const uint8_t*)\"{name}\", {len}U }}, // name (StringView)\n",
name = contract.name,
len = name_bytes
));
out.push_str(&format!(
" {{ (const uint8_t*)\"{name}\", {len}U }}, // contract_name (StringView)\n",
name = contract_name_full,
len = name_bytes_full
));
out.push_str(&format!(
" {{ {}U, {}U, {}U }} // version (Version)\n",
contract.version.major, contract.version.minor, contract.version.patch
));
out.push_str(" };\n");
out.push_str(&format!(
" AbiError err_{upper}{{}};\n host->register_guest_contract(host, &desc_{upper}, &polyplug_plugin::{upper}_INTERFACE, &err_{upper});\n",
upper = upper
));
out.push_str(&format!(
" if (err_{}.code != static_cast<uint32_t>(AbiErrorCode::Ok)) return err_{};\n\n",
upper, upper
));
Ok(())
}
fn generate_host_callers_hpp(ir: &ValidatedIr) -> Result<String, PolyplugcError> {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"types.hpp\"\n");
out.push_str("#include \"polyplug/error.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include \"polyplug/runtime.hpp\"\n");
out.push_str("#include <array>\n");
out.push_str("#include <atomic>\n");
out.push_str("#include <cstddef>\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include <memory>\n");
out.push_str("#include <optional>\n\n");
out.push_str("namespace polyplug_generated {\n\n");
if let Some(ref bundle) = ir.bundle {
out.push_str(&format!(
"static constexpr uint64_t MY_BUNDLE_ID = {}ULL;\n\n",
bundle.bundle_id
));
}
emit_cpp_revision_helper(&mut out);
if ir.contracts.iter().any(contract_needs_arena) {
emit_cpp_call_arena_helpers(&mut out);
}
for contract in &ir.contracts {
generate_cpp_host_contract(&mut out, contract)?;
}
out.push_str("} // namespace polyplug_generated\n");
Ok(out)
}
fn generate_manifest_toml() -> String {
let mut out: String = String::new();
out.push_str("# THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n");
out.push_str("[manifest]\n");
out.push_str("schema_version = 1\n");
out.push_str("lang = \"cpp\"\n");
out.push_str("generated_by = \"polyplugc\"\n");
out
}
fn generate_bundle_manifest_cpp(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().cloned().collect();
let fn_count_entries: Vec<String> = ir
.contracts
.iter()
.filter(|c: &&ResolvedContract| {
provides_set.contains(&format!("{}@{}", c.name, c.version.major))
})
.map(|c: &ResolvedContract| {
let fn_count: u32 = c.functions.len() as u32;
format!("\"{}@{}\" = {}", c.name, c.version.major, fn_count)
})
.collect();
let function_count_toml: String = format!("{{ {} }}", fn_count_entries.join(", "));
let dep_tables: String = super::emit_manifest_dependencies(&bundle.dependencies);
let reinit: bool = bundle.needs_reinit_on_dep_reload;
let loader: &str = "native";
format!(
"# THIS FILE IS AUTO-GENERATED BY polyplugc. DO NOT EDIT.\n\
name = \"{name}\"\n\
id = {bundle_id}\n\
version = \"{version}\"\n\
loader = \"{loader}\"\n\
provides = {provides_toml}\n\
function_count = {function_count_toml}\n\
needs_reinit_on_dep_reload = {reinit}\n\
{file_field}\n\
{dep_tables}",
bundle_id = bundle.bundle_id
)
}
fn substitute_variant_refs_cpp(
declared_variants: &[EnumVariant],
expr: &str,
enum_name: &str,
repr_cpp: &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!(
"static_cast<{}>({}::{})",
repr_cpp, enum_name, ident
));
} else {
result.push_str(&ident);
}
} else {
result.push(c);
i += 1;
}
}
result
}
fn generate_cpp_enum(out: &mut String, e: &EnumDef) {
let repr_cpp: &str = e.repr.cpp_name();
out.push_str(&format!("/// Enum `{}` (repr: {})\n", e.name, repr_cpp));
out.push_str(&format!("enum class {} : {} {{\n", e.name, repr_cpp));
for variant in &e.variants {
let subst_value: String =
substitute_variant_refs_cpp(&e.variants, &variant.value, &e.name, repr_cpp);
out.push_str(&format!(" {} = {},\n", variant.name, subst_value));
}
out.push_str("};\n");
if e.bitflag {
out.push_str(&format!(
"inline {} operator|({} a, {} b) {{ return static_cast<{}>(static_cast<{}>(a) | static_cast<{}>(b)); }}\n",
e.name, e.name, e.name, e.name, repr_cpp, repr_cpp
));
out.push_str(&format!(
"inline {} operator&({} a, {} b) {{ return static_cast<{}>(static_cast<{}>(a) & static_cast<{}>(b)); }}\n",
e.name, e.name, e.name, e.name, repr_cpp, repr_cpp
));
out.push_str(&format!(
"inline {} operator~({} a) {{ return static_cast<{}>(~static_cast<{}>(a)); }}\n",
e.name, e.name, e.name, repr_cpp
));
}
out.push('\n');
}
fn generate_cpp_type(out: &mut String, ty: &ResolvedType) {
out.push_str(&format!("/// User-defined type `{}`\n", ty.name));
out.push_str("struct ");
out.push_str(&ty.name);
out.push_str(" {\n");
for field in &ty.fields {
out.push_str(&format!(
" {} {};\n",
cpp_types_hpp_type_name(&field.ty),
field.name
));
}
out.push_str("};\n\n");
}
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 emit_cpp_revision_helper(out: &mut String) {
out.push_str("/// Read the runtime's registry revision through `revision_ptr` with one\n");
out.push_str("/// acquire atomic load. Returns 0 when the pointer is null (no runtime),\n");
out.push_str("/// making the per-call staleness check a no-op.\n");
out.push_str(
"inline uint64_t polyplug_load_revision(const uint64_t* revision_ptr) noexcept {\n",
);
out.push_str(" if (revision_ptr == nullptr) { return 0; }\n");
out.push_str(
" // SAFETY: revision_ptr was returned by HostApi.revision_counter and points at\n",
);
out.push_str(
" // the runtime's revision counter — a Rust AtomicU64, layout-compatible with\n",
);
out.push_str(
" // std::atomic<std::uint64_t> — whose address is stable for the runtime's lifetime.\n",
);
out.push_str(
" return reinterpret_cast<const std::atomic<std::uint64_t>*>(revision_ptr)->load(std::memory_order_acquire);\n",
);
out.push_str("}\n\n");
}
fn emit_cpp_call_arena_helpers(out: &mut String) {
out.push_str("/// Size of each caller's inline call-arena buffer.\n");
out.push_str("///\n");
out.push_str("/// Variable-size VM return values (strings, buffers) are bump-allocated from\n");
out.push_str("/// this buffer; outputs larger than it spill into host-allocated overflow\n");
out.push_str("/// blocks that are retained across resets and freed only at teardown.\n");
out.push_str(&format!(
"static constexpr size_t CALL_ARENA_BUF_LEN = {CALL_ARENA_BUF_LEN};\n\n"
));
out.push_str("/// Minimum size of a host-allocated overflow block, including its header.\n");
out.push_str("static constexpr size_t POLYPLUG_OVERFLOW_BLOCK_MIN = 4096;\n");
out.push_str("/// Alignment used for host-allocated overflow blocks.\n");
out.push_str(
"static constexpr size_t POLYPLUG_OVERFLOW_BLOCK_ALIGN = alignof(ArenaOverflowBlock);\n\n",
);
out.push_str("/// Bump-allocate `size` bytes aligned to `align` within `[from, end)`.\n");
out.push_str("/// Returns nullptr if the request does not fit.\n");
out.push_str(
"inline uint8_t* polyplug_arena_bump(uint8_t* from, uint8_t* end, size_t size, size_t align) noexcept {\n",
);
out.push_str(" auto addr = reinterpret_cast<size_t>(from);\n");
out.push_str(" size_t aligned = (addr + (align - 1)) & ~(align - 1);\n");
out.push_str(" if (aligned < addr) { return nullptr; } // overflow on alignment\n");
out.push_str(" size_t new_cur = aligned + size;\n");
out.push_str(" if (new_cur < aligned) { return nullptr; } // overflow on size\n");
out.push_str(" if (new_cur <= reinterpret_cast<size_t>(end)) {\n");
out.push_str(" return reinterpret_cast<uint8_t*>(aligned);\n");
out.push_str(" }\n");
out.push_str(" return nullptr;\n");
out.push_str("}\n\n");
out.push_str("/// Try to bump-allocate `size`@`align` from `block`'s free region.\n");
out.push_str("///\n");
out.push_str("/// Advances `block->used` on success and returns the allocation pointer.\n");
out.push_str(
"/// Returns nullptr if the request does not fit in the block's remaining room.\n",
);
out.push_str(
"inline uint8_t* polyplug_arena_serve_from_block(ArenaOverflowBlock* block, size_t size, size_t align) noexcept {\n",
);
out.push_str(
" // SAFETY: block is a valid overflow block previously allocated by polyplug_arena_alloc;\n",
);
out.push_str(
" // reading used/capacity and deriving pointers from the block base stays within\n",
);
out.push_str(" // the capacity-byte allocation.\n");
out.push_str(" auto block_bytes = reinterpret_cast<uint8_t*>(block);\n");
out.push_str(" uint8_t* from = block_bytes + block->used;\n");
out.push_str(" uint8_t* end = block_bytes + block->capacity;\n");
out.push_str(" uint8_t* p = polyplug_arena_bump(from, end, size, align);\n");
out.push_str(" if (p == nullptr) { return nullptr; }\n");
out.push_str(
" // SAFETY: block is a valid chain node; writing used (a plain size_t field)\n",
);
out.push_str(
" // is in-bounds because the block was allocated with at least sizeof(ArenaOverflowBlock) bytes.\n",
);
out.push_str(" block->used = static_cast<size_t>(p - block_bytes) + size;\n");
out.push_str(" return p;\n");
out.push_str("}\n\n");
out.push_str("/// Allocate `size` bytes aligned to `align` from `arena`.\n");
out.push_str("///\n");
out.push_str("/// Serves from the primary region by bumping `cur`; on exhaustion, walks the\n");
out.push_str(
"/// retained overflow chain for a block with spare room; if none fits, requests a\n",
);
out.push_str("/// fresh overflow block from the host and serves from it. Returns nullptr if\n");
out.push_str(
"/// `size == 0`, if `align` is not a power of two, or if a host allocation fails.\n",
);
out.push_str("/// The returned pointer is valid until the next polyplug_arena_reset().\n");
out.push_str(
"inline uint8_t* polyplug_arena_alloc(CallArena* arena, size_t size, size_t align) noexcept {\n",
);
out.push_str(" if (size == 0 || align == 0 || (align & (align - 1)) != 0) {\n");
out.push_str(" return nullptr;\n");
out.push_str(" }\n");
out.push_str(
" if (uint8_t* p = polyplug_arena_bump(arena->cur, arena->end, size, align)) {\n",
);
out.push_str(" arena->cur = p + size;\n");
out.push_str(" return p;\n");
out.push_str(" }\n");
out.push_str(" if (arena->host == nullptr) { return nullptr; }\n");
out.push_str(
" // REUSE PASS: walk the retained chain; serve from the first block with room.\n",
);
out.push_str(
" for (ArenaOverflowBlock* b = arena->first_overflow; b != nullptr; b = b->next) {\n",
);
out.push_str(
" if (uint8_t* p = polyplug_arena_serve_from_block(b, size, align)) { return p; }\n",
);
out.push_str(" }\n");
out.push_str(" // ALLOCATE NEW: no retained block had enough room.\n");
out.push_str(" size_t header = sizeof(ArenaOverflowBlock);\n");
out.push_str(" size_t needed = header + align + size;\n");
out.push_str(" size_t capacity = needed > POLYPLUG_OVERFLOW_BLOCK_MIN ? needed : POLYPLUG_OVERFLOW_BLOCK_MIN;\n");
out.push_str(
" // SAFETY: arena->host is non-null (checked above) and valid for the arena's\n",
);
out.push_str(
" // lifetime. The allocator returns a block of `capacity` bytes or nullptr.\n",
);
out.push_str(
" auto block_ptr = static_cast<uint8_t*>(arena->host->alloc(arena->host, capacity, POLYPLUG_OVERFLOW_BLOCK_ALIGN));\n",
);
out.push_str(" if (block_ptr == nullptr) { return nullptr; }\n");
out.push_str(" // SAFETY: block_ptr is aligned for ArenaOverflowBlock and owns at least\n");
out.push_str(" // `capacity >= header` bytes, so writing the header is sound.\n");
out.push_str(" auto block = reinterpret_cast<ArenaOverflowBlock*>(block_ptr);\n");
out.push_str(" block->next = arena->first_overflow;\n");
out.push_str(" block->capacity = capacity;\n");
out.push_str(" block->used = header;\n");
out.push_str(" arena->first_overflow = block;\n");
out.push_str(" return polyplug_arena_serve_from_block(block, size, align);\n");
out.push_str("}\n\n");
out.push_str(
"/// Rewind `arena` for reuse: the primary region and every retained overflow block\n",
);
out.push_str(
"/// become available again. Overflow blocks are NOT freed — they are retained for\n",
);
out.push_str(
"/// reuse across calls; call polyplug_arena_free_all() at teardown to free them.\n",
);
out.push_str(
"/// After reset, all pointers previously returned by polyplug_arena_alloc are invalid.\n",
);
out.push_str("inline void polyplug_arena_reset(CallArena* arena) noexcept {\n");
out.push_str(" arena->cur = arena->base;\n");
out.push_str(" ArenaOverflowBlock* block = arena->first_overflow;\n");
out.push_str(" while (block != nullptr) {\n");
out.push_str(
" // SAFETY: every block in the chain was allocated by polyplug_arena_alloc\n",
);
out.push_str(" // with a valid header; reading next and writing used are in-bounds.\n");
out.push_str(" block->used = sizeof(ArenaOverflowBlock);\n");
out.push_str(" block = block->next;\n");
out.push_str(" }\n");
out.push_str("}\n\n");
out.push_str("/// Free all retained overflow blocks and reset the overflow chain to empty.\n");
out.push_str("/// Call this at teardown (destructor) to release all host-allocated memory.\n");
out.push_str("inline void polyplug_arena_free_all(CallArena* arena) noexcept {\n");
out.push_str(" ArenaOverflowBlock* block = arena->first_overflow;\n");
out.push_str(" while (block != nullptr) {\n");
out.push_str(
" // SAFETY: every block was allocated by polyplug_arena_alloc with a valid\n",
);
out.push_str(" // header; reading next/capacity before freeing is sound.\n");
out.push_str(" ArenaOverflowBlock* next = block->next;\n");
out.push_str(" size_t capacity = block->capacity;\n");
out.push_str(" if (arena->host != nullptr) {\n");
out.push_str(
" // SAFETY: block was allocated by host->alloc with these exact args.\n",
);
out.push_str(
" arena->host->free(arena->host, reinterpret_cast<uint8_t*>(block), capacity, POLYPLUG_OVERFLOW_BLOCK_ALIGN);\n",
);
out.push_str(" }\n");
out.push_str(" block = next;\n");
out.push_str(" }\n");
out.push_str(" arena->first_overflow = nullptr;\n");
out.push_str("}\n\n");
out.push_str(
"/// Construct a CallArena over `buf` (primary region) with `host` for overflow.\n",
);
out.push_str("inline CallArena polyplug_arena_new(uint8_t* buf, size_t len, const HostApi* host) noexcept {\n");
out.push_str(" CallArena arena{};\n");
out.push_str(" arena.cur = buf;\n");
out.push_str(" arena.end = buf + len;\n");
out.push_str(" arena.base = buf;\n");
out.push_str(" arena.host = host;\n");
out.push_str(" arena.first_overflow = nullptr;\n");
out.push_str(" return arena;\n");
out.push_str("}\n\n");
}
fn generate_cpp_host_contract(
out: &mut String,
contract: &ResolvedContract,
) -> Result<(), PolyplugcError> {
let class_name: String = contract_name_to_class(&contract.name);
let _contract_upper: String = contract.name.to_uppercase().replace(['.', '-'], "_");
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("/// - `create()`: resolves handle and calls `create_instance`\n");
out.push_str("/// - destructor: 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");
out.push_str("///\n");
out.push_str(
"/// Methods returning variable-size values (`StringView`, `Buffer`, or structs\n",
);
out.push_str(
"/// that may embed one) are non-const and reset this caller's arena at the start\n",
);
out.push_str(
"/// of the call. Any view returned by such a method borrows arena memory and is\n",
);
out.push_str("/// valid only until the next arena-backed call on the same caller.\n");
}
out.push_str(&format!("class {} {{\npublic:\n", class_name));
out.push_str(" /// Factory method - creates instance or nullopt if not found.\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(" /// - `std::optional<Self>` if interface found and instance created\n");
out.push_str(" /// - `std::nullopt` if interface not found or `create_instance` failed\n");
out.push_str(&format!(
" static std::optional<{}> create(GuestContractHandle handle, const HostApi* host) noexcept {{\n",
class_name
));
out.push_str(" if (host == nullptr) {\n");
out.push_str(" return std::nullopt;\n");
out.push_str(" }\n");
out.push_str(" // Resolve the interface from the handle via HostApi method.\n");
out.push_str(" const GuestContractInterface* iface = host->resolve_guest_contract(host, handle);\n");
out.push_str(" if (iface == nullptr) {\n");
out.push_str(" return std::nullopt;\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(
" GuestContractInstance instance{};\n host->create_guest_instance(host, iface, nullptr, &instance);\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(" const uint64_t* revision_ptr = host->revision_counter(host);\n");
out.push_str(
" const uint64_t cached_revision = polyplug_load_revision(revision_ptr);\n",
);
out.push_str(&format!(
" return {}(iface, instance, host, handle, revision_ptr, cached_revision);\n",
class_name
));
out.push_str(" }\n\n");
out.push_str(" /// Destructor - calls `destroy_instance` to clean up.\n");
out.push_str(&format!(" ~{}() noexcept {{\n", class_name));
if needs_arena {
out.push_str(
" // Free any overflow blocks the arena still holds before destruction.\n",
);
out.push_str(" // arena_buf_ is null only on a moved-from caller.\n");
out.push_str(" if (arena_buf_) {\n");
out.push_str(" polyplug_arena_free_all(&arena_);\n");
out.push_str(" }\n");
}
out.push_str(
" // If the registry changed since we resolved, the cached interface and\n",
);
out.push_str(
" // instance are stale — a reload/unload reclaimed their backing — so calling\n",
);
out.push_str(
" // the dead interface's destroy would be UB; the reload/unload already\n",
);
out.push_str(" // reclaimed the instance, so skip the destroy entirely.\n");
out.push_str(" if (polyplug_load_revision(revision_ptr_) != cached_revision_) {\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" // Destroy instance via factory\n");
out.push_str(" // SAFETY: instance was created by create_instance and is valid.\n");
out.push_str(" if (instance_.data != nullptr) {\n");
out.push_str(" host_->destroy_guest_instance(host_, interface_, instance_);\n");
out.push_str(" instance_.data = nullptr; // Prevent reuse after cleanup.\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" // Move-only (instance handles are unique)\n");
out.push_str(&format!(
" {}({}&& other) noexcept\n",
class_name, class_name
));
out.push_str(" : interface_(other.interface_),\n");
out.push_str(" instance_(other.instance_),\n");
out.push_str(" host_(other.host_),\n");
out.push_str(" handle_(other.handle_),\n");
out.push_str(" revision_ptr_(other.revision_ptr_),\n");
if needs_arena {
out.push_str(" cached_revision_(other.cached_revision_),\n");
out.push_str(" arena_buf_(std::move(other.arena_buf_)),\n");
out.push_str(" arena_(other.arena_) {\n");
} else {
out.push_str(" cached_revision_(other.cached_revision_) {\n");
}
out.push_str(" other.instance_.data = nullptr; // Prevent double-destroy.\n");
out.push_str(" }\n");
out.push_str(&format!(
" {}& operator=({}&& other) noexcept {{\n",
class_name, class_name
));
out.push_str(" if (this != &other) {\n");
if needs_arena {
out.push_str(" // Release this caller's overflow blocks before overwriting.\n");
out.push_str(" if (arena_buf_) {\n");
out.push_str(" polyplug_arena_free_all(&arena_);\n");
out.push_str(" }\n");
}
out.push_str(
" // Destroy current instance first — but only if the registry has not\n",
);
out.push_str(
" // changed under us; a reload/unload already reclaimed a stale instance.\n",
);
out.push_str(" if (instance_.data != nullptr && polyplug_load_revision(revision_ptr_) == cached_revision_) {\n");
out.push_str(" host_->destroy_guest_instance(host_, interface_, instance_);\n");
out.push_str(" }\n");
out.push_str(" interface_ = other.interface_; instance_ = other.instance_; host_ = other.host_; other.instance_.data = nullptr;\n");
out.push_str(" handle_ = other.handle_; revision_ptr_ = other.revision_ptr_; cached_revision_ = other.cached_revision_;\n");
if needs_arena {
out.push_str(
" arena_buf_ = std::move(other.arena_buf_); arena_ = other.arena_;\n",
);
}
out.push_str(" }\n");
out.push_str(" return *this;\n");
out.push_str(" }\n");
out.push_str(&format!(
" {}(const {}&) = delete;\n",
class_name, class_name
));
out.push_str(&format!(
" {}& operator=(const {}&) = delete;\n\n",
class_name, class_name
));
out.push_str(" /// Check if this caller holds a resolved contract interface.\n");
out.push_str(" /// Keys off the interface pointer, not `instance_.data`: stateless\n");
out.push_str(
" /// contracts legitimately use a null instance as an opaque dispatch token.\n",
);
out.push_str(
" explicit operator bool() const noexcept { return interface_ != nullptr; }\n\n",
);
out.push_str(" /// Check if this caller holds a resolved contract interface.\n");
out.push_str(" bool is_valid() const noexcept { return interface_ != nullptr; }\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(" void reset() noexcept {\n");
out.push_str(
" // If the registry changed under us, the cached interface/instance are\n",
);
out.push_str(
" // stale (a reload/unload reclaimed their backing). revalidate() abandons\n",
);
out.push_str(
" // the dead instance and builds a fresh one on the current interface —\n",
);
out.push_str(" // exactly the fresh instance reset() promises — so defer to it.\n");
out.push_str(" if (polyplug_load_revision(revision_ptr_) != cached_revision_) {\n");
out.push_str(" revalidate();\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" if (instance_.data != nullptr) {\n");
out.push_str(" host_->destroy_guest_instance(host_, interface_, instance_);\n");
out.push_str(" }\n");
out.push_str(" instance_ = GuestContractInstance{};\n");
out.push_str(" host_->create_guest_instance(host_, interface_, nullptr, &instance_);\n");
out.push_str(" }\n\n");
for func in &contract.functions {
generate_cpp_host_function(out, &class_name, func)?;
}
out.push_str("private:\n");
out.push_str(" /// Re-resolve the cached interface after the registry changed under us.\n");
out.push_str(" ///\n");
out.push_str(
" /// A hot-reload swapped a new interface into the same slot, so the retained\n",
);
out.push_str(
" /// handle still resolves — to the new interface; an unload vacated the slot,\n",
);
out.push_str(
" /// so it resolves to null and `false` is returned (the contract 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 state it created are already epoch-reclaimed, so\n",
);
out.push_str(" /// calling the dead interface's destroy would be UB.\n");
out.push_str(" bool revalidate() noexcept {\n");
out.push_str(" if (host_ == nullptr) {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(" const GuestContractInterface* iface = host_->resolve_guest_contract(host_, handle_);\n");
out.push_str(" if (iface == nullptr) {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(" GuestContractInstance inst{};\n");
out.push_str(" host_->create_guest_instance(host_, iface, nullptr, &inst);\n");
out.push_str(" interface_ = iface;\n");
out.push_str(" instance_ = inst;\n");
out.push_str(" cached_revision_ = polyplug_load_revision(revision_ptr_);\n");
out.push_str(" return true;\n");
out.push_str(" }\n\n");
out.push_str(" /// Resolved interface pointer from the registry.\n");
out.push_str(" const GuestContractInterface* interface_;\n");
out.push_str(" /// Instance handle created by `create_instance`.\n");
out.push_str(" GuestContractInstance instance_;\n");
out.push_str(" /// Host interface pointer (needed for create/destroy_instance).\n");
out.push_str(" const HostApi* host_;\n");
out.push_str(
" /// 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(" GuestContractHandle handle_;\n");
out.push_str(" /// Pointer to the runtime's registry revision counter, fetched once via\n");
out.push_str(
" /// `HostApi.revision_counter`. Polled before each dispatch (one atomic load,\n",
);
out.push_str(" /// no call into the runtime); null when there is no runtime.\n");
out.push_str(" const uint64_t* revision_ptr_;\n");
out.push_str(
" /// Revision value read when the interface was resolved. Compared before each\n",
);
out.push_str(
" /// dispatch against the live counter to detect a reload/unload and re-resolve,\n",
);
out.push_str(" /// so the cached interface pointer never dangles.\n");
out.push_str(" uint64_t cached_revision_;\n");
if needs_arena {
out.push_str(
" /// Stable-address backing buffer for the per-call arena. Held by unique_ptr\n",
);
out.push_str(" /// so the arena's interior pointers survive moving the caller value.\n");
out.push_str(" std::unique_ptr<std::array<uint8_t, CALL_ARENA_BUF_LEN>> arena_buf_;\n");
out.push_str(
" /// Per-call bump arena over `arena_buf_`, reset at each arena-backed call.\n",
);
out.push_str(" CallArena arena_;\n");
}
out.push('\n');
if needs_arena {
out.push_str(&format!(
" explicit {}(const GuestContractInterface* iface, GuestContractInstance inst, const HostApi* host, GuestContractHandle handle, const uint64_t* revision_ptr, uint64_t cached_revision)\n",
class_name
));
out.push_str(" : interface_(iface), instance_(inst), host_(host), handle_(handle), revision_ptr_(revision_ptr), cached_revision_(cached_revision),\n");
out.push_str(
" arena_buf_(std::make_unique<std::array<uint8_t, CALL_ARENA_BUF_LEN>>()),\n",
);
out.push_str(
" arena_(polyplug_arena_new(arena_buf_->data(), CALL_ARENA_BUF_LEN, host)) {}\n",
);
} else {
out.push_str(&format!(
" explicit {}(const GuestContractInterface* iface, GuestContractInstance inst, const HostApi* host, GuestContractHandle handle, const uint64_t* revision_ptr, uint64_t cached_revision) noexcept\n",
class_name
));
out.push_str(" : interface_(iface), instance_(inst), host_(host), handle_(handle), revision_ptr_(revision_ptr), cached_revision_(cached_revision) {}\n");
}
out.push_str("};\n\n");
Ok(())
}
fn generate_cpp_host_function(
out: &mut String,
class_name: &str,
func: &ResolvedFunction,
) -> Result<(), PolyplugcError> {
let return_type: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
let params: Vec<String> = func
.params
.iter()
.map(|p| format!("{} {}", cpp_type_name(&p.ty), p.name))
.collect();
let params_str: String = params.join(", ");
out.push_str(&format!(
" /// Call `{}` (function_id={})\n",
func.name, func.function_id
));
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",
);
out.push_str(" /// the next arena-backed call on this caller.\n");
}
out.push_str(&format!(
" {} {}({}) {{\n",
return_type, func.name, params_str
));
out.push_str(
" // Per-call staleness check: re-resolve before dispatch if the registry changed,\n",
);
out.push_str(" // so the cached interface pointer is never used once it dangles.\n");
out.push_str(" if (polyplug_load_revision(revision_ptr_) != cached_revision_ && !revalidate()) {\n");
out.push_str(" static constexpr const char* err_msg = \"contract not found\";\n");
out.push_str(
" polyplug::check_abi_error(AbiError{static_cast<uint32_t>(AbiErrorCode::NotFound), StringView{reinterpret_cast<const uint8_t*>(err_msg), 18}});\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(" polyplug_arena_reset(&arena_);\n");
}
let args_ptr_code: String = build_args_ptr_code(class_name, func);
out.push_str(&args_ptr_code);
let fn_id: u32 = func.function_id;
let is_void_return: bool = matches!(
func.returns.as_ref(),
None | Some(ResolvedTypeRef::AbiType(AbiBuiltin::Void))
);
let null_iface_msg: &str = "interface is null";
out.push_str(" // SAFETY: interface_ is valid for the lifetime of this wrapper.\n");
out.push_str(" if (!interface_) {\n");
out.push_str(&format!(
" static constexpr const char* err_msg = \"{null_iface_msg}\";\n"
));
out.push_str(&format!(
" polyplug::check_abi_error(AbiError{{static_cast<uint32_t>(AbiErrorCode::InvalidPointer), StringView{{reinterpret_cast<const uint8_t*>(err_msg), {len}}}}});\n",
len = null_iface_msg.len()
));
out.push_str(" }\n");
let out_ptr_expr: &str = if is_void_return {
out.push_str(" void* out_ptr = nullptr;\n");
"out_ptr"
} else {
out.push_str(&format!(" {} out{{}};\n", return_type));
out.push_str(" void* out_ptr = &out;\n");
"out_ptr"
};
out.push_str(" AbiError err{};\n");
out.push_str(" switch (interface_->dispatch_type) {\n");
out.push_str(" case DispatchType::Native: {\n");
let fn_unavailable_msg: &str = "function not available in interface";
out.push_str(&format!(
" if ({}U >= interface_->dispatch.native.function_count) {{\n",
fn_id
));
out.push_str(&format!(
" static constexpr const char* err_msg = \"{fn_unavailable_msg}\";\n"
));
out.push_str(&format!(
" polyplug::check_abi_error(AbiError{{static_cast<uint32_t>(AbiErrorCode::FunctionNotAvailable), StringView{{reinterpret_cast<const uint8_t*>(err_msg), {len}}}}});\n",
len = fn_unavailable_msg.len()
));
out.push_str(" }\n");
out.push_str(&format!(
" auto fn_ = reinterpret_cast<void(*)(GuestContractInstance, const void*, void*, AbiError*)>(interface_->dispatch.native.functions[{}U]);\n",
fn_id
));
out.push_str(" // SAFETY: instance_ is the token returned by create_instance and is valid.\n");
out.push_str(" // args_ptr/out_ptr match the ABI contract for this function.\n");
out.push_str(&format!(
" fn_(instance_, args_ptr, {}, &err);\n",
out_ptr_expr
));
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType::VirtualMachine: {\n");
let arena_arg: &str = if needs_arena { "&arena_" } else { "nullptr" };
out.push_str(&format!(
" (interface_->dispatch.vm.call)(interface_->dispatch.vm.loader_data, instance_, {}U, args_ptr, {}, {}, &err);\n",
fn_id, out_ptr_expr, arena_arg
));
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" polyplug::check_abi_error(err);\n");
if !is_void_return {
out.push_str(" return out;\n");
}
out.push_str(" }\n\n");
Ok(())
}
fn build_args_ptr_code(class_name: &str, func: &ResolvedFunction) -> String {
if func.params.is_empty() {
return " const void* args_ptr = nullptr;\n".to_owned();
}
if func.params.len() == 1 {
let param: &ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::UserDefined(_) => {
return format!(" const void* args_ptr = &{};\n", param.name);
}
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => {
let cpp_ty: String = cpp_type_name(¶m.ty);
return format!(
" const {cpp_ty} local_{name} = {name};\n const void* args_ptr = &local_{name};\n",
cpp_ty = cpp_ty,
name = param.name
);
}
}
}
let func_name_cap: String = capitalise_first(&func.name);
let struct_name: String = format!("{}{}{}", class_name, func_name_cap, "Args");
let mut code: String = String::new();
code.push_str(&format!(" struct {} {{", struct_name));
for param in &func.params {
let cpp_ty: String = cpp_type_name(¶m.ty);
code.push_str(&format!(" {} {};", cpp_ty, param.name));
}
code.push_str(" };\n");
let field_inits: Vec<String> = func.params.iter().map(|p| p.name.clone()).collect();
code.push_str(&format!(
" {} args_val{{ {} }};\n",
struct_name,
field_inits.join(", ")
));
code.push_str(" const void* args_ptr = &args_val;\n");
code
}
fn qualified_user_type(name: &str) -> String {
format!("polyplug_generated::{name}")
}
fn cpp_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.cpp_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Buffer".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "void*".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => qualified_user_type(name),
}
}
fn cpp_types_hpp_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::UserDefined(name) => name.clone(),
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => cpp_type_name(ty),
}
}
fn contract_name_to_class(name: &str) -> String {
name.split('.')
.map(|p| {
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 contract_name_to_guest_contract_class(name: &str) -> String {
name.split('.')
.map(|p| {
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_lower_snake(name: &str) -> String {
name.replace('.', "_")
}
fn contract_name_to_upper_snake(name: &str) -> String {
name.replace('.', "_").to_uppercase()
}
fn capitalise_first(s: &str) -> String {
let mut chars: core::str::Chars<'_> = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn host_contract_name_to_cpp_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| {
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 generate_cpp_host_contract_trait(out: &mut String, contract: &ResolvedHostContract) {
let class_name: String = host_contract_name_to_cpp_trait(&contract.name);
out.push_str(&format!(
"/// Host abstract class for contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str("/// Hosts implement this class to provide functionality to plugins.\n");
out.push_str(&format!("class {} {{\npublic:\n", class_name));
out.push_str(&format!(" virtual ~{}() = default;\n", class_name));
for func in &contract.functions {
generate_cpp_host_trait_method(out, func);
}
out.push_str("};\n\n");
}
fn host_contract_name_to_cpp_caller(name: &str) -> String {
let name_without_prefix: &str = name.strip_prefix("host.").unwrap_or(name);
let pascal: String = name_without_prefix
.split('.')
.map(|p| {
let mut chars: core::str::Chars<'_> = p.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join("");
if pascal.starts_with("Host") {
pascal + "Contract"
} else {
"Host".to_owned() + &pascal + "Contract"
}
}
fn cpp_guest_caller_param_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.cpp_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "std::string_view".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Buffer".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "void*".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => format!("const {}&", qualified_user_type(name)),
}
}
fn cpp_guest_caller_return_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::Primitive(p) => p.cpp_name().to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "std::string_view".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "std::span<const std::uint8_t>".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Ptr) => "void*".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Void) => "void".to_owned(),
ResolvedTypeRef::UserDefined(name) => qualified_user_type(name),
}
}
fn generate_cpp_guest_host_contract_caller(out: &mut String, contract: &ResolvedHostContract) {
let class_name: String = host_contract_name_to_cpp_caller(&contract.name);
out.push_str(&format!(
"/// Guest caller for host contract `{}` (id=0x{:016X})\n",
contract.name, contract.contract_id
));
out.push_str("/// Plugins use this class to call host-provided functionality.\n");
out.push_str(&format!("class {} {{\npublic:\n", class_name));
out.push_str(" /// Factory method - creates caller from HostApi or nullopt if not found.\n");
out.push_str(&format!(
" static std::optional<{}> from_host(const HostApi* host, uint32_t min_version = 0) noexcept {{\n",
class_name
));
out.push_str(" if (host == nullptr) {\n");
out.push_str(" return std::nullopt;\n");
out.push_str(" }\n");
out.push_str(&format!(
" HostContractInstance instance = host->get_host_contract(host, 0x{:016X}ULL, min_version);\n",
contract.contract_id
));
out.push_str(" if (instance.data == nullptr) {\n");
out.push_str(" return std::nullopt;\n");
out.push_str(" }\n");
out.push_str(&format!(
" const HostContractInterface* interface = host->resolve_host_contract_interface(host, 0x{:016X}ULL, min_version);\n",
contract.contract_id
));
out.push_str(" if (interface == nullptr) {\n");
out.push_str(" return std::nullopt;\n");
out.push_str(" }\n");
out.push_str(&format!(
" return {}(host, interface, instance);\n",
class_name
));
out.push_str(" }\n\n");
out.push_str(" /// Check if caller is valid (interface and instance are non-null).\n");
out.push_str(" bool is_valid() const noexcept { return interface_ != nullptr && instance_.data != nullptr; }\n\n");
out.push_str(" /// Explicit bool conversion for validity check.\n");
out.push_str(" explicit operator bool() const noexcept { return interface_ != nullptr && instance_.data != nullptr; }\n\n");
for func in &contract.functions {
generate_cpp_guest_host_contract_method(out, func, &class_name);
}
out.push_str("private:\n");
out.push_str(&format!(
" explicit {}(const HostApi* host, const HostContractInterface* interface, HostContractInstance instance) noexcept\n",
class_name
));
out.push_str(" : host_(host), interface_(interface), instance_(instance) {}\n\n");
out.push_str(" // Host interface captured in from_host — used for failure logging.\n");
out.push_str(" // No DSO-global host storage exists; the pointer flows per caller.\n");
out.push_str(" const HostApi* host_;\n");
out.push_str(" const HostContractInterface* interface_;\n");
out.push_str(" HostContractInstance instance_;\n");
out.push_str("};\n\n");
}
fn emit_cpp_log_call_failure_helper(out: &mut String) {
out.push_str("#ifndef POLYPLUG_GENERATED_LOG_CALL_FAILURE\n");
out.push_str("#define POLYPLUG_GENERATED_LOG_CALL_FAILURE\n");
out.push_str("namespace detail {\n\n");
out.push_str("/// Log a failed cross-boundary call through the host logging funnel\n");
out.push_str("/// (level 1 = Error) before the caller returns its default value.\n");
out.push_str("inline void log_call_failure(const HostApi* host, const char* scope, const char* what, uint32_t code) noexcept {\n");
out.push_str(" if (host == nullptr) {\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" char message[96];\n");
out.push_str(" const int written = std::snprintf(message, sizeof(message), \"%s failed: code=%u\", what, code);\n");
out.push_str(
" const std::size_t length = written > 0 ? static_cast<std::size_t>(written) : 0U;\n",
);
out.push_str(" host->log(host, 1U,\n");
out.push_str(
" StringView{reinterpret_cast<const uint8_t*>(scope), std::strlen(scope)},\n",
);
out.push_str(" StringView{reinterpret_cast<const uint8_t*>(message), length});\n");
out.push_str("}\n\n");
out.push_str("} // namespace detail\n");
out.push_str("#endif // POLYPLUG_GENERATED_LOG_CALL_FAILURE\n\n");
}
fn generate_cpp_guest_host_contract_method(
out: &mut String,
func: &ResolvedFunction,
class_name: &str,
) {
let fn_id: u32 = func.function_id;
let return_type: String = func
.returns
.as_ref()
.map(cpp_guest_caller_return_type_name)
.unwrap_or_else(|| "void".to_owned());
let params: Vec<String> = func
.params
.iter()
.map(|p| format!("{} {}", cpp_guest_caller_param_type_name(&p.ty), p.name))
.collect();
let params_str: String = params.join(", ");
out.push_str(&format!(
" /// Call host contract function `{}` (function_id={})\n",
func.name, fn_id
));
out.push_str(&format!(
" {} {}({}) noexcept {{\n",
return_type, func.name, params_str
));
let what: String = format!("{}.{}", class_name, func.name);
let default_return: String = if func.returns.is_some() {
format!("return {}{{}};", return_type)
} else {
"return;".to_owned()
};
out.push_str(" if (interface_ == nullptr) {\n");
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.host_caller\", \"{what}\", static_cast<uint32_t>(AbiErrorCode::InvalidPointer));\n"
));
out.push_str(&format!(" {default_return}\n"));
out.push_str(" }\n\n");
emit_cpp_guest_host_contract_args_setup(out, func, class_name);
emit_cpp_guest_host_contract_out_setup(out, &func.returns);
out.push_str(" AbiError err{};\n");
out.push_str(" switch (interface_->dispatch_type) {\n");
out.push_str(" case DispatchType::Native: {\n");
out.push_str(&format!(
" if ({fn_id}U >= interface_->dispatch.native.function_count) {{\n"
));
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.host_caller\", \"{what}\", static_cast<uint32_t>(AbiErrorCode::FunctionNotAvailable));\n"
));
out.push_str(&format!(" {default_return}\n"));
out.push_str(" }\n");
out.push_str(&format!(
" auto fn_ = reinterpret_cast<void(*)(HostContractInstance, const void*, void*, AbiError*)>(interface_->dispatch.native.functions[{fn_id}U]);\n"
));
out.push_str(" fn_(instance_, args_ptr, out_ptr, &err);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType::VirtualMachine: {\n");
out.push_str(&format!(
" (interface_->dispatch.vm.call)(interface_->dispatch.vm.loader_data, GuestContractInstance{{}}, {fn_id}U, args_ptr, out_ptr, nullptr, &err);\n"
));
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" if (err.code != static_cast<uint32_t>(AbiErrorCode::Ok)) {\n");
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.host_caller\", \"{what}\", err.code);\n"
));
out.push_str(&format!(" {default_return}\n"));
out.push_str(" }\n\n");
if let Some(ret_ty) = &func.returns {
let expr: String = cpp_guest_caller_return_expr(ret_ty);
out.push_str(&format!(" return {};\n", expr));
}
out.push_str(" }\n\n");
}
fn emit_cpp_guest_host_contract_args_setup(
out: &mut String,
func: &ResolvedFunction,
class_name: &str,
) {
if func.params.is_empty() {
out.push_str(" const void* args_ptr = nullptr;\n");
return;
}
if func.params.len() == 1 {
let param: &crate::ir::ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" StringView {}_view{{ reinterpret_cast<const uint8_t*>({}.data()), {}.size() }};\n",
param.name, param.name, param.name
));
out.push_str(&format!(
" const void* args_ptr = &{}_view;\n",
param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" const void* args_ptr = &{};\n",
param.name
));
}
ResolvedTypeRef::Primitive(_) | ResolvedTypeRef::AbiType(_) => {
let cpp_ty: String = cpp_type_name(¶m.ty);
out.push_str(&format!(
" const {cpp_ty} local_{name} = {name};\n",
cpp_ty = cpp_ty,
name = param.name
));
out.push_str(&format!(
" const void* args_ptr = &local_{name};\n",
name = param.name
));
}
}
return;
}
let func_name_cap: String = capitalise_first(&func.name);
let struct_name: String = format!("{}{}Args", class_name, func_name_cap);
out.push_str(&format!(" struct {} {{", struct_name));
for param in &func.params {
let cpp_ty: String = cpp_type_name(¶m.ty);
out.push_str(&format!(" {} {};", cpp_ty, param.name));
}
out.push_str(" };\n");
let field_inits: Vec<String> = func
.params
.iter()
.map(|p| match &p.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
format!(
"StringView{{ reinterpret_cast<const uint8_t*>({}.data()), {}.size() }}",
p.name, p.name
)
}
_ => p.name.clone(),
})
.collect();
out.push_str(&format!(
" {} args_val{{ {} }};\n",
struct_name,
field_inits.join(", ")
));
out.push_str(" const void* args_ptr = &args_val;\n");
}
fn cpp_guest_caller_out_local_type_name(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => "StringView".to_owned(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => "Buffer".to_owned(),
_ => cpp_guest_caller_return_type_name(ty),
}
}
fn cpp_guest_caller_return_expr(ty: &ResolvedTypeRef) -> String {
match ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
"polyplug::to_string_view(out)".to_owned()
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
"out.ptr ? std::span<const std::uint8_t>(out.ptr, out.len) : std::span<const std::uint8_t>{}".to_owned()
}
_ => "out".to_owned(),
}
}
fn emit_cpp_guest_host_contract_out_setup(out: &mut String, returns: &Option<ResolvedTypeRef>) {
if let Some(ret_ty) = returns {
let abi_ty: String = cpp_guest_caller_out_local_type_name(ret_ty);
out.push_str(&format!(" {} out{{}};\n", abi_ty));
out.push_str(" void* out_ptr = &out;\n");
} else {
out.push_str(" void* out_ptr = nullptr;\n");
}
}
fn generate_cpp_guest_host_contracts_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str(
"// Re-generate with: polyplugc generate --bundle bundle.toml --lang cpp --out <dir>\n",
);
out.push_str("#pragma once\n");
out.push_str("#include \"types.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include \"polyplug/guest.hpp\"\n");
out.push_str("#include <cstddef>\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include <cstdio>\n");
out.push_str("#include <cstring>\n");
out.push_str("#include <optional>\n");
out.push_str("#include <string_view>\n\n");
out.push_str("namespace polyplug_plugin {\n\n");
emit_cpp_log_call_failure_helper(&mut out);
for contract in &ir.host_contracts {
generate_cpp_guest_host_contract_caller(&mut out, contract);
}
for contract in &ir.host_contracts {
let class_name: String = host_contract_name_to_cpp_caller(&contract.name);
let const_name: String = class_name.to_uppercase() + "_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!(
"constexpr uint64_t {} = 0x{:016X}ULL;\n\n",
const_name, contract.contract_id
));
}
out.push_str("} // namespace polyplug_plugin\n");
out
}
fn generate_cpp_host_trait_method(out: &mut String, func: &ResolvedFunction) {
let return_type: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
let params: Vec<String> = func
.params
.iter()
.map(|p| {
let cpp_ty: String = cpp_type_name(&p.ty);
match &p.ty {
ResolvedTypeRef::UserDefined(_) => format!("const {}& {}", cpp_ty, p.name),
ResolvedTypeRef::AbiType(_) => format!("{} {}", cpp_ty, p.name),
ResolvedTypeRef::Primitive(_) => format!("{} {}", cpp_ty, p.name),
}
})
.collect();
let params_str: String = params.join(", ");
out.push_str(&format!(
" virtual {} {}({}) = 0;\n",
return_type, func.name, params_str
));
}
fn generate_cpp_host_contracts_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"types.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include <cstdint>\n\n");
out.push_str("namespace polyplug_host {\n\n");
for contract in &ir.host_contracts {
generate_cpp_host_contract_trait(&mut out, contract);
}
for contract in &ir.host_contracts {
let trait_name: String = host_contract_name_to_cpp_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!(
"constexpr uint64_t {} = 0x{:016X}ULL;\n\n",
const_name, contract.contract_id
));
}
out.push_str("} // namespace polyplug_host\n");
out
}
fn generate_cpp_host_interface_factories_file(ir: &ValidatedIr) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str("// Re-generate with: polyplugc generate --api api.toml --lang cpp --out <dir>\n");
out.push_str("#pragma once\n");
out.push_str("#include \"host_contracts.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include <memory>\n\n");
out.push_str("namespace polyplug_host {\n\n");
for contract in &ir.host_contracts {
generate_cpp_host_interface_factory(&mut out, contract);
}
out.push_str("} // namespace polyplug_host\n");
out
}
fn generate_cpp_host_interface_factory(out: &mut String, contract: &ResolvedHostContract) {
let trait_name: String = host_contract_name_to_cpp_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;
let patch: u32 = contract.version.patch;
let singleton: bool = contract.singleton;
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 inherit from the abstract class.\n");
out.push_str("///\n");
out.push_str("/// # Memory\n");
out.push_str("/// The returned interface pointer is valid for the lifetime of the program.\n");
out.push_str("/// The implementation unique_ptr is released and managed internally.\n");
out.push_str("template<typename T>\n");
out.push_str(&format!(
"const HostContractInterface* {}(std::unique_ptr<T> impl) noexcept {{\n",
factory_name
));
out.push_str(" T* impl_ptr = impl.release();\n\n");
for func in &contract.functions {
generate_cpp_host_thunk(out, func, &contract.name, &trait_name);
}
out.push_str(&format!(
" static void* const FUNCTIONS[{}] = {{\n",
fn_count
));
for func in &contract.functions {
let thunk_name: String = format!(
"{}_{}_thunk",
contract.name.replace('.', "_").to_lowercase(),
func.name
);
out.push_str(&format!(
" reinterpret_cast<void*>({}),\n",
thunk_name
));
}
out.push_str(" };\n\n");
out.push_str(" // create_instance stub - host owns the singleton instance lifecycle\n");
out.push_str(
" static constexpr HostContractInterface_create_instance_fn create_instance_stub =\n",
);
out.push_str(
" +[](const HostContractInterface* self, const void* /*args*/, HostContractInstance* out_instance) noexcept -> void {\n",
);
out.push_str(" if (out_instance == nullptr) return;\n");
out.push_str(" // Write the registrant-owned user_data as the instance; the thunks\n");
out.push_str(" // recover the implementation from it (no mutable static state — the\n");
out.push_str(" // interface itself is heap-allocated per factory call).\n");
out.push_str(" *out_instance = HostContractInstance{self->user_data};\n");
out.push_str(" };\n\n");
out.push_str(" // destroy_instance stub - host owns the singleton instance lifecycle\n");
out.push_str(
" static constexpr HostContractInterface_destroy_instance_fn destroy_instance_stub =\n",
);
out.push_str(" +[](const HostContractInterface* /*this*/, HostContractInstance /*instance*/) noexcept -> void {\n");
if singleton {
out.push_str(
" // Singleton: no-op, the implementation lives for program lifetime\n",
);
} else {
out.push_str(
" // Multi-instance: not supported in host-side factory, use custom factory\n",
);
}
out.push_str(" };\n\n");
out.push_str(" auto* iface = new HostContractInterface{\n");
out.push_str(&format!(
" 0x{contract_id:016X}ULL, // contract_id\n"
));
out.push_str(&format!(
" Version{{{major}U, {minor}U, {patch}U}}, // contract_version\n"
));
out.push_str(&format!(" {}, // singleton\n", singleton));
out.push_str(" DispatchType::Native, // dispatch_type\n");
out.push_str(" nullptr, // runtime (set by polyplug during registration)\n");
out.push_str(" nullptr, // user_data (set below to the registrant-owned impl)\n");
out.push_str(" create_instance_stub, // create_instance\n");
out.push_str(" destroy_instance_stub, // destroy_instance\n");
out.push_str(" DispatchMechanisms{ .native = NativeDispatch{\n");
out.push_str(&format!(" {fn_count}U, // function_count\n"));
out.push_str(" FUNCTIONS, // functions\n");
out.push_str(" } }, // dispatch.native\n");
out.push_str(" }; // dispatch\n\n");
out.push_str(
" // Route the implementation through user_data; create_instance reads it via `this`.\n",
);
out.push_str(" iface->user_data = static_cast<void*>(impl_ptr);\n");
out.push_str(" return iface;\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("/// * `loader_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 pointer is valid for the lifetime of the program.\n");
out.push_str(&format!(
"const HostContractInterface* {}(\n",
factory_vm_name
));
out.push_str(" void* loader_data,\n");
out.push_str(" VmDispatch_call_fn dispatch_fn\n");
out.push_str(") noexcept {\n");
out.push_str(" // create_instance stub - VM loader owns instance lifecycle\n");
out.push_str(
" static constexpr HostContractInterface_create_instance_fn vm_create_instance_stub =\n",
);
out.push_str(
" +[](const HostContractInterface* /*this*/, const void* /*args*/, HostContractInstance* out_instance) noexcept -> void {\n",
);
out.push_str(" if (out_instance == nullptr) return;\n");
out.push_str(" // VM dispatch: instance managed by VM loader, write placeholder\n");
out.push_str(" *out_instance = HostContractInstance{nullptr};\n");
out.push_str(" };\n\n");
out.push_str(" // destroy_instance stub - VM loader owns instance lifecycle\n");
out.push_str(" static constexpr HostContractInterface_destroy_instance_fn vm_destroy_instance_stub =\n");
out.push_str(" +[](const HostContractInterface* /*this*/, HostContractInstance /*instance*/) noexcept -> void {\n");
out.push_str(" // VM dispatch: instance managed by VM loader, no-op here\n");
out.push_str(" };\n\n");
out.push_str(" auto* iface = new HostContractInterface{\n");
out.push_str(&format!(
" 0x{contract_id:016X}ULL, // contract_id\n"
));
out.push_str(&format!(
" Version{{{major}U, {minor}U, {patch}U}}, // contract_version\n"
));
out.push_str(&format!(" {}, // singleton\n", singleton));
out.push_str(" DispatchType::VirtualMachine, // dispatch_type\n");
out.push_str(" nullptr, // runtime (set by polyplug during registration)\n");
out.push_str(" loader_data, // user_data (registrant-owned VM bridge data)\n");
out.push_str(" vm_create_instance_stub, // create_instance\n");
out.push_str(" vm_destroy_instance_stub, // destroy_instance\n");
out.push_str(" DispatchMechanisms{ .vm = VmDispatch{\n");
out.push_str(" dispatch_fn, // call\n");
out.push_str(" VmLoaderData{loader_data}, // loader_data\n");
out.push_str(" } }, // dispatch.vm\n");
out.push_str(" }; // dispatch\n");
out.push_str(" return iface;\n");
out.push_str("}\n\n");
}
fn generate_cpp_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(&format!(
" static constexpr auto {} = +[](HostContractInstance instance, const void* args, void* out, AbiError* out_err) noexcept -> void {{\n",
thunk_name
));
out.push_str(&format!(
" auto* impl = static_cast<{}*>(instance.data);\n",
trait_name
));
out.push_str(" if (impl == nullptr) {\n");
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Panic), StringView{nullptr, 0}};\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(" try {\n");
if !func.params.is_empty() {
generate_cpp_host_thunk_args(out, func);
} else {
out.push_str(" (void)args;\n");
}
generate_cpp_host_thunk_call(out, func, has_return);
if has_return {
let ret_ty: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
out.push_str(&format!(
" *static_cast<{}*>(out) = result;\n",
ret_ty
));
} else {
out.push_str(" (void)out;\n");
}
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Ok), StringView{nullptr, 0}};\n");
out.push_str(" } catch (...) {\n");
out.push_str(" *out_err = AbiError{static_cast<uint32_t>(AbiErrorCode::Panic), StringView{nullptr, 0}};\n");
out.push_str(" }\n");
out.push_str(" };\n\n");
}
fn generate_cpp_host_thunk_args(out: &mut String, func: &ResolvedFunction) {
if func.params.len() == 1 {
let param: &crate::ir::ResolvedParam = &func.params[0];
let ty_name: String = cpp_type_name(¶m.ty);
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" StringView {} = *static_cast<const StringView*>(args);\n",
param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" Buffer {}_buf = *static_cast<const Buffer*>(args);\n",
param.name
));
out.push_str(&format!(
" (void){}_buf; // Buffer handling depends on use case\n",
param.name
));
}
ResolvedTypeRef::UserDefined(_) => {
out.push_str(&format!(
" const {}& {} = *static_cast<const {}*>(args);\n",
ty_name, param.name, ty_name
));
}
_ => {
out.push_str(&format!(
" {} {} = *static_cast<const {}*>(args);\n",
ty_name, param.name, ty_name
));
}
}
} else {
let pack_struct: String = format!("{}Args", func.name.to_uppercase());
out.push_str(&format!(" struct {} {{\n", pack_struct));
for param in &func.params {
let cpp_ty: String = cpp_type_name(¶m.ty);
out.push_str(&format!(" {} {};\n", cpp_ty, param.name));
}
out.push_str(" };\n");
out.push_str(&format!(
" const {}* packed = static_cast<const {}*>(args);\n",
pack_struct, pack_struct
));
for param in &func.params {
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => {
out.push_str(&format!(
" StringView {} = packed->{};\n",
param.name, param.name
));
}
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => {
out.push_str(&format!(
" Buffer {} = packed->{}; (void){};\n",
param.name, param.name, param.name
));
}
_ => {
let cpp_ty: String = cpp_type_name(¶m.ty);
out.push_str(&format!(
" {} {} = packed->{};\n",
cpp_ty, param.name, param.name
));
}
}
}
}
}
fn generate_cpp_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: &crate::ir::ResolvedParam = &func.params[0];
match ¶m.ty {
ResolvedTypeRef::AbiType(AbiBuiltin::StringView) => param.name.clone(),
ResolvedTypeRef::AbiType(AbiBuiltin::Buffer) => param.name.clone(),
ResolvedTypeRef::UserDefined(_) => param.name.clone(),
_ => param.name.clone(),
}
} else {
func.params
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", ")
};
if has_return {
let ret_ty: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
out.push_str(&format!(
" {} result = impl->{}({});\n",
ret_ty, func.name, call_args
));
} else {
out.push_str(&format!(
" impl->{}({});\n",
func.name, call_args
));
}
}
const _: fn() = || {
let _ = cpp_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U8));
};
fn generate_cpp_peer_callers_file(ir: &ValidatedIr, peers: &[&ResolvedContract]) -> String {
let mut out: String = String::new();
out.push_str(CPP_FILE_HEADER);
out.push_str(
"// Re-generate with: polyplugc generate --bundle bundle.toml --lang cpp --out <dir>\n",
);
out.push_str("#pragma once\n");
out.push_str("#include \"types.hpp\"\n");
out.push_str("#include \"polyplug/guest.hpp\"\n");
out.push_str("#include \"polyplug/abi.hpp\"\n");
out.push_str("#include <array>\n");
out.push_str("#include <atomic>\n");
out.push_str("#include <cstddef>\n");
out.push_str("#include <cstdint>\n");
out.push_str("#include <cstdio>\n");
out.push_str("#include <cstring>\n");
out.push_str("#include <memory>\n");
out.push_str("#include <optional>\n\n");
out.push_str("namespace polyplug_plugin {\n\n");
emit_cpp_log_call_failure_helper(&mut out);
emit_cpp_revision_helper(&mut out);
let any_needs_arena: bool = peers
.iter()
.any(|c: &&ResolvedContract| contract_needs_arena(c));
if any_needs_arena {
emit_cpp_call_arena_helpers(&mut out);
}
for contract in peers {
let min_ver: u32 = peer_min_version(ir, contract.contract_id);
generate_cpp_peer_caller(&mut out, contract, min_ver);
}
out.push_str("} // namespace polyplug_plugin\n");
out
}
fn generate_cpp_peer_caller(out: &mut String, contract: &ResolvedContract, min_version: u32) {
let class_name: String = format!("{}Peer", contract_name_to_class(&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",
);
out.push_str(
"/// path as the host->guest caller; no host-mediated round-trip, no per-call registry\n",
);
out.push_str(
"/// resolve, no epoch pin. The declared dependency keeps the peer alive; a hot-reload\n",
);
out.push_str("/// is caught by the cached revision counter.\n");
out.push_str("/// Use `resolve(host)` to obtain an instance; returns `std::nullopt` when\n");
out.push_str("/// the contract is not registered or the host interface is null.\n");
if needs_arena {
out.push_str("///\n");
out.push_str("/// # Call-arena lifetime\n");
out.push_str("///\n");
out.push_str(
"/// Methods returning variable-size values (`StringView`, `Buffer`, or structs\n",
);
out.push_str(
"/// that may embed one) are non-const and reset this caller's arena at the start\n",
);
out.push_str(
"/// of the call. Any view returned by such a method borrows arena memory and is\n",
);
out.push_str("/// valid only until the next arena-backed call on the same caller.\n");
}
out.push_str(&format!("class {} {{\npublic:\n", class_name));
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 HostApi pointer handed to the author\n");
out.push_str(" /// factory (`polyplug_create_<plugin>`) — no DSO-global host exists.\n");
out.push_str(" ///\n");
out.push_str(" /// Returns `std::nullopt` if the host interface is null, the contract\n");
out.push_str(" /// is not registered, or `resolve_guest_contract` returns null.\n");
out.push_str(&format!(
" static std::optional<{}> resolve(const HostApi* host) noexcept {{\n",
class_name
));
out.push_str(" if (host == nullptr) {\n");
out.push_str(" return std::nullopt;\n");
out.push_str(" }\n");
out.push_str(&format!(
" GuestContractHandle handle = host->find_guest_contract(host, 0x{:016X}ULL, {}U);\n",
contract.contract_id, min_version
));
out.push_str(" // Pass the handle straight to resolve_guest_contract; it rejects\n");
out.push_str(" // stale/invalid handles and returns nullptr — do not inspect fields.\n");
out.push_str(
" const GuestContractInterface* iface = host->resolve_guest_contract(host, handle);\n",
);
out.push_str(" if (iface == nullptr) {\n");
out.push_str(" return std::nullopt;\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(
" GuestContractInstance instance{};\n host->create_guest_instance(host, iface, nullptr, &instance);\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 and\n",
);
out.push_str(" // re-resolve before dispatching.\n");
out.push_str(" const uint64_t* revision_ptr = host->revision_counter(host);\n");
out.push_str(
" const uint64_t cached_revision = polyplug_load_revision(revision_ptr);\n",
);
out.push_str(&format!(
" return {}(iface, instance, host, handle, revision_ptr, cached_revision);\n",
class_name
));
out.push_str(" }\n\n");
out.push_str(" /// Destructor - calls `destroy_instance` to clean up.\n");
out.push_str(&format!(" ~{}() noexcept {{\n", class_name));
if needs_arena {
out.push_str(
" // Free any overflow blocks the arena still holds before destruction.\n",
);
out.push_str(" // arena_buf_ is null only on a moved-from caller.\n");
out.push_str(" if (arena_buf_) {\n");
out.push_str(" polyplug_arena_reset(&arena_);\n");
out.push_str(" }\n");
}
out.push_str(
" // If the registry changed since we resolved, the cached interface and\n",
);
out.push_str(
" // instance are stale — a reload/unload reclaimed their backing — so calling\n",
);
out.push_str(
" // the dead interface's destroy would be UB; the reload/unload already\n",
);
out.push_str(" // reclaimed the instance, so skip the destroy entirely.\n");
out.push_str(" if (polyplug_load_revision(revision_ptr_) != cached_revision_) {\n");
out.push_str(" return;\n");
out.push_str(" }\n");
out.push_str(
" // SAFETY: instance was created by create_instance on the resolved interface.\n",
);
out.push_str(" // We guard on instance.data to skip the call for stateless (null-data) contracts.\n");
out.push_str(" if (instance_.data != nullptr) {\n");
out.push_str(" host_->destroy_guest_instance(host_, iface_, instance_);\n");
out.push_str(" instance_.data = nullptr; // Prevent reuse after cleanup.\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" // Move-only (instance handles are unique)\n");
out.push_str(&format!(
" {}({}&& other) noexcept\n",
class_name, class_name
));
out.push_str(" : iface_(other.iface_),\n");
out.push_str(" instance_(other.instance_),\n");
out.push_str(" host_(other.host_),\n");
out.push_str(" handle_(other.handle_),\n");
out.push_str(" revision_ptr_(other.revision_ptr_),\n");
if needs_arena {
out.push_str(" cached_revision_(other.cached_revision_),\n");
out.push_str(" arena_buf_(std::move(other.arena_buf_)),\n");
out.push_str(" arena_(other.arena_) {\n");
} else {
out.push_str(" cached_revision_(other.cached_revision_) {\n");
}
out.push_str(" other.instance_.data = nullptr; // Prevent double-destroy.\n");
out.push_str(" }\n");
out.push_str(&format!(
" {}& operator=({}&& other) noexcept {{\n",
class_name, class_name
));
out.push_str(" if (this != &other) {\n");
if needs_arena {
out.push_str(" if (arena_buf_) {\n");
out.push_str(" polyplug_arena_reset(&arena_);\n");
out.push_str(" }\n");
}
out.push_str(
" // Destroy current instance first — but only if the registry has not\n",
);
out.push_str(
" // changed under us; a reload/unload already reclaimed a stale instance.\n",
);
out.push_str(" if (instance_.data != nullptr && polyplug_load_revision(revision_ptr_) == cached_revision_) {\n");
out.push_str(" host_->destroy_guest_instance(host_, iface_, instance_);\n");
out.push_str(" }\n");
out.push_str(
" iface_ = other.iface_; instance_ = other.instance_; host_ = other.host_; other.instance_.data = nullptr;\n",
);
out.push_str(" handle_ = other.handle_; revision_ptr_ = other.revision_ptr_; cached_revision_ = other.cached_revision_;\n");
if needs_arena {
out.push_str(
" arena_buf_ = std::move(other.arena_buf_); arena_ = other.arena_;\n",
);
}
out.push_str(" }\n");
out.push_str(" return *this;\n");
out.push_str(" }\n");
out.push_str(&format!(
" {}(const {}&) = delete;\n",
class_name, class_name
));
out.push_str(&format!(
" {}& operator=(const {}&) = delete;\n\n",
class_name, class_name
));
out.push_str(" /// Check if this peer holds a resolved (non-null) interface.\n");
out.push_str(" /// Keys off the interface pointer, not `instance_.data`: stateless\n");
out.push_str(
" /// contracts legitimately use a null instance as an opaque dispatch token.\n",
);
out.push_str(" explicit operator bool() const noexcept { return iface_ != nullptr; }\n\n");
out.push_str(" /// Check if this peer holds a resolved (non-null) interface.\n");
out.push_str(" bool is_valid() const noexcept { return iface_ != nullptr; }\n\n");
for func in &contract.functions {
generate_cpp_peer_fn_caller(out, &class_name, func);
}
out.push_str("private:\n");
out.push_str(
" /// Re-resolve the cached peer interface after the registry changed under us.\n",
);
out.push_str(" ///\n");
out.push_str(
" /// A hot-reload swapped a new interface into the same slot, so the retained\n",
);
out.push_str(
" /// handle still resolves — to the new interface; an unload vacated the slot,\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 state it created are already epoch-reclaimed, so\n",
);
out.push_str(" /// calling the dead interface's destroy would be UB. Dispatch then goes\n");
out.push_str(" /// straight through the freshly resolved interface pointer.\n");
out.push_str(" bool revalidate() noexcept {\n");
out.push_str(" if (host_ == nullptr) {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(" const GuestContractInterface* iface = host_->resolve_guest_contract(host_, handle_);\n");
out.push_str(" if (iface == nullptr) {\n");
out.push_str(" return false;\n");
out.push_str(" }\n");
out.push_str(" GuestContractInstance inst{};\n");
out.push_str(" host_->create_guest_instance(host_, iface, nullptr, &inst);\n");
out.push_str(" iface_ = iface;\n");
out.push_str(" instance_ = inst;\n");
out.push_str(" cached_revision_ = polyplug_load_revision(revision_ptr_);\n");
out.push_str(" return true;\n");
out.push_str(" }\n\n");
out.push_str(" /// Resolved interface pointer for the peer contract.\n");
out.push_str(" const GuestContractInterface* iface_;\n");
out.push_str(" /// Instance handle created from the peer interface.\n");
out.push_str(" GuestContractInstance instance_;\n");
out.push_str(
" /// Host interface pointer used for instance lifecycle (create/destroy) and\n /// re-resolve — NOT for dispatch, which goes straight through the interface.\n",
);
out.push_str(" const HostApi* host_;\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(" GuestContractHandle handle_;\n");
out.push_str(" /// Pointer to the runtime's registry revision counter, fetched once via\n");
out.push_str(
" /// `HostApi.revision_counter`. Polled before each dispatch (one atomic load,\n",
);
out.push_str(" /// no call into the runtime); null when there is no runtime.\n");
out.push_str(" const uint64_t* revision_ptr_;\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\n",
);
out.push_str(" /// cached interface pointer and instance never dangle.\n");
out.push_str(" uint64_t cached_revision_;\n");
if needs_arena {
out.push_str(
" /// Stable-address backing buffer for the per-call arena. Held by unique_ptr\n",
);
out.push_str(" /// so the arena's interior pointers survive moving the caller value.\n");
out.push_str(" std::unique_ptr<std::array<uint8_t, CALL_ARENA_BUF_LEN>> arena_buf_;\n");
out.push_str(
" /// Per-call bump arena over `arena_buf_`, reset at each arena-backed call.\n",
);
out.push_str(" CallArena arena_;\n");
}
out.push('\n');
if needs_arena {
out.push_str(&format!(
" explicit {}(const GuestContractInterface* iface, GuestContractInstance inst, const HostApi* host, GuestContractHandle handle, const uint64_t* revision_ptr, uint64_t cached_revision)\n",
class_name
));
out.push_str(" : iface_(iface), instance_(inst), host_(host), handle_(handle), revision_ptr_(revision_ptr), cached_revision_(cached_revision),\n");
out.push_str(
" arena_buf_(std::make_unique<std::array<uint8_t, CALL_ARENA_BUF_LEN>>()),\n",
);
out.push_str(
" arena_(polyplug_arena_new(arena_buf_->data(), CALL_ARENA_BUF_LEN, host)) {}\n",
);
} else {
out.push_str(&format!(
" explicit {}(const GuestContractInterface* iface, GuestContractInstance inst, const HostApi* host, GuestContractHandle handle, const uint64_t* revision_ptr, uint64_t cached_revision) noexcept\n",
class_name
));
out.push_str(" : iface_(iface), instance_(inst), host_(host), handle_(handle), revision_ptr_(revision_ptr), cached_revision_(cached_revision) {}\n");
}
out.push_str("};\n\n");
}
fn generate_cpp_peer_fn_caller(out: &mut String, class_name: &str, func: &ResolvedFunction) {
let fn_id: u32 = func.function_id;
let needs_arena: bool = fn_needs_arena(func);
let return_type: String = func
.returns
.as_ref()
.map(cpp_type_name)
.unwrap_or_else(|| "void".to_owned());
let params: Vec<String> = func
.params
.iter()
.map(|p| format!("{} {}", cpp_type_name(&p.ty), p.name))
.collect();
let params_str: String = params.join(", ");
let self_qual: &str = "";
out.push_str(&format!(
" /// Call peer function `{}` (function_id={}) via direct interface dispatch.\n",
func.name, fn_id
));
if needs_arena {
out.push_str(
" /// Returns a value borrowing this caller's arena; it stays valid until\n",
);
out.push_str(" /// the next arena-backed call on this caller.\n");
}
out.push_str(&format!(
" {} {}({}){} {{\n",
return_type, func.name, params_str, self_qual
));
out.push_str(" if (iface_ == nullptr) {\n");
if func.returns.is_some() {
out.push_str(&format!(" return {}{{}};\n", return_type));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n");
out.push_str(" if (polyplug_load_revision(revision_ptr_) != cached_revision_ && !revalidate()) {\n");
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.peer_caller\", \"{class_name}.{fn_name}\", static_cast<uint32_t>(AbiErrorCode::NotFound));\n",
fn_name = func.name
));
if func.returns.is_some() {
out.push_str(&format!(" return {}{{}};\n", return_type));
} else {
out.push_str(" return;\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(" polyplug_arena_reset(&arena_);\n");
}
let args_ptr_code: String = build_args_ptr_code(class_name, func);
out.push_str(&args_ptr_code);
let is_void_return: bool = matches!(
func.returns.as_ref(),
None | Some(ResolvedTypeRef::AbiType(AbiBuiltin::Void))
);
if is_void_return {
out.push_str(" void* out_ptr = nullptr;\n");
} else {
out.push_str(&format!(" {} out{{}};\n", return_type));
out.push_str(" void* out_ptr = &out;\n");
}
out.push_str(" AbiError err{};\n");
out.push_str(" switch (iface_->dispatch_type) {\n");
out.push_str(" case DispatchType::Native: {\n");
out.push_str(&format!(
" if ({}U >= iface_->dispatch.native.function_count) {{\n",
fn_id
));
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.peer_caller\", \"{class_name}.{fn_name}\", static_cast<uint32_t>(AbiErrorCode::FunctionNotAvailable));\n",
fn_name = func.name
));
if func.returns.is_some() {
out.push_str(&format!(
" return {}{{}};\n",
return_type
));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n");
out.push_str(&format!(
" auto fn_ = reinterpret_cast<void(*)(GuestContractInstance, const void*, void*, AbiError*)>(iface_->dispatch.native.functions[{}U]);\n",
fn_id
));
out.push_str(" // SAFETY: instance_ is the token returned by create_instance and is valid.\n");
out.push_str(" // args_ptr/out_ptr match the ABI contract for this function.\n");
out.push_str(" fn_(instance_, args_ptr, out_ptr, &err);\n");
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" case DispatchType::VirtualMachine: {\n");
let arena_arg: &str = if needs_arena { "&arena_" } else { "nullptr" };
out.push_str(&format!(
" (iface_->dispatch.vm.call)(iface_->dispatch.vm.loader_data, instance_, {}U, args_ptr, out_ptr, {}, &err);\n",
fn_id, arena_arg
));
out.push_str(" break;\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" if (err.code != static_cast<uint32_t>(AbiErrorCode::Ok)) {\n");
out.push_str(&format!(
" detail::log_call_failure(host_, \"guest.peer_caller\", \"{class_name}.{fn_name}\", err.code);\n",
fn_name = func.name
));
if func.returns.is_some() {
out.push_str(&format!(" return {}{{}};\n", return_type));
} else {
out.push_str(" return;\n");
}
out.push_str(" }\n");
if !is_void_return {
out.push_str(" return out;\n");
}
out.push_str(" }\n\n");
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::ir::ReprType;
use crate::ir::ResolvedDependency;
use crate::ir::ResolvedParam;
use crate::ir::Version;
#[test]
fn class_name_conversion() {
assert_eq!(
contract_name_to_class("image.decode"),
"ImageDecodeContract"
);
}
#[test]
fn plugin_class_name_conversion() {
assert_eq!(
contract_name_to_guest_contract_class("test.add"),
"TestAddGuestContract"
);
}
#[test]
fn lower_snake_conversion() {
assert_eq!(contract_name_to_lower_snake("test.add"), "test_add");
}
#[test]
fn upper_snake_conversion() {
assert_eq!(contract_name_to_upper_snake("test.add"), "TEST_ADD");
}
#[test]
fn generate_host_empty_ir() {
let generator: CppGenerator = CppGenerator;
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!(!files.files.is_empty());
assert!(
files
.files
.iter()
.any(|f| f.content.contains("AUTO-GENERATED"))
);
}
#[test]
fn generate_guest_empty_ir() {
let generator: CppGenerator = CppGenerator;
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");
assert_eq!(files.files.len(), 4);
let names: Vec<String> = files
.files
.iter()
.map(|f| f.path.to_string_lossy().to_string())
.collect();
assert!(names.contains(&"guest/types.hpp".to_owned()));
assert!(names.contains(&"guest/contracts.hpp".to_owned()));
assert!(names.contains(&"guest/interfaces.hpp".to_owned()));
assert!(names.contains(&"guest/init.hpp".to_owned()));
}
#[test]
fn generate_cpp_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_cpp_enum(&mut out, &e);
assert!(
out.contains("enum class PixelFormat : uint32_t"),
"missing enum class: {out}"
);
assert!(
out.contains("Unknown = 0"),
"missing Unknown variant: {out}"
);
assert!(
!out.contains("operator|"),
"non-bitflag should not have operator|: {out}"
);
}
#[test]
fn generate_cpp_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_cpp_enum(&mut out, &e);
assert!(
out.contains("enum class ImageFlags : uint32_t"),
"missing enum class: {out}"
);
assert!(out.contains("operator|"), "missing operator|: {out}");
assert!(out.contains("operator&"), "missing operator&: {out}");
assert!(out.contains("operator~"), "missing operator~: {out}");
assert!(
out.contains("static_cast<uint32_t>"),
"missing static_cast: {out}"
);
}
#[test]
fn generate_cpp_host_contract_has_factory_method() {
let contract = ResolvedContract {
name: "test.add".to_owned(),
contract_id: 0x123456789ABCDEF0,
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::I32),
},
ResolvedParam {
name: "b".to_owned(),
ty: ResolvedTypeRef::Primitive(PrimitiveType::I32),
},
],
returns: Some(ResolvedTypeRef::Primitive(PrimitiveType::I32)),
}],
};
let mut out: String = String::new();
generate_cpp_host_contract(&mut out, &contract).unwrap();
assert!(
out.contains("static std::optional<TestAddContract> create(GuestContractHandle handle, const HostApi* host) noexcept"),
"missing factory method: {out}"
);
assert!(
out.contains("return interface_ != nullptr;"),
"validity check must key off interface_, not instance_.data: {out}"
);
let native_arm_pos: usize = out
.find("case DispatchType::Native: {")
.expect("missing Native dispatch arm");
let bounds_check_pos: usize = out
.find("interface_->dispatch.native.function_count")
.expect("bounds check must use dispatch.native.function_count");
assert!(
bounds_check_pos > native_arm_pos,
"bounds check must be inside the Native dispatch arm, not before the branch: {out}"
);
assert!(
out.contains("switch (interface_->dispatch_type)"),
"dispatch must branch on dispatch_type: {out}"
);
assert!(
out.contains("GuestContractInstance instance_"),
"missing instance member: {out}"
);
assert!(
out.contains("const GuestContractInterface* interface_"),
"missing interface member: {out}"
);
assert!(
out.contains("const HostApi* host_"),
"missing host member: {out}"
);
assert!(
out.contains("bool is_valid() const noexcept"),
"missing is_valid method: {out}"
);
assert!(
out.contains("explicit operator bool() const noexcept"),
"missing operator bool: {out}"
);
assert!(
out.contains("void reset() noexcept"),
"missing reset method: {out}"
);
assert!(
out.contains("~TestAddContract() noexcept"),
"missing destructor: {out}"
);
assert!(
out.contains("host_->destroy_guest_instance(host_, interface_, instance_)"),
"missing destroy_guest_instance call in destructor: {out}"
);
assert!(
out.contains("host->create_guest_instance(host, iface, nullptr, &instance)"),
"missing create_guest_instance call in factory: {out}"
);
assert!(
out.contains("TestAddContract(TestAddContract&& other) noexcept"),
"missing move constructor: {out}"
);
assert!(
out.contains("other.instance_.data = nullptr"),
"missing nulling of moved-from instance: {out}"
);
assert!(
out.contains("TestAddContract(const TestAddContract&) = delete"),
"missing deleted copy constructor: {out}"
);
assert!(
out.contains("explicit TestAddContract(const GuestContractInterface* iface, GuestContractInstance inst, const HostApi* host, GuestContractHandle handle, const uint64_t* revision_ptr, uint64_t cached_revision) noexcept"),
"missing private constructor: {out}"
);
assert!(
out.contains("fn_(instance_, args_ptr,") && out.contains(", &err)"),
"dispatch must call fn_ with instance_, args_ptr, out_ptr, &err: {out}"
);
assert!(
out.contains("const uint64_t* revision_ptr_;")
&& out.contains("uint64_t cached_revision_;"),
"missing revision_ptr_/cached_revision_ members: {out}"
);
assert!(
out.contains("GuestContractHandle handle_;"),
"missing retained contract handle_ member: {out}"
);
assert!(
out.contains("host->revision_counter(host)"),
"factory must fetch the revision counter once: {out}"
);
assert!(
out.contains("bool revalidate() noexcept"),
"missing revalidate() helper: {out}"
);
assert!(
out.contains(
"if (polyplug_load_revision(revision_ptr_) != cached_revision_ && !revalidate())"
),
"dispatch must re-resolve before use when the revision changed: {out}"
);
assert!(
out.contains("if (polyplug_load_revision(revision_ptr_) != cached_revision_) {\n return;\n }"),
"destructor must skip destroy when the revision changed: {out}"
);
}
#[test]
fn host_contract_name_to_cpp_trait_conversion() {
assert_eq!(host_contract_name_to_cpp_trait("host.logger"), "HostLogger");
assert_eq!(
host_contract_name_to_cpp_trait("host.fs.reader"),
"HostFsReader"
);
assert_eq!(
host_contract_name_to_cpp_trait("host.HostLogger"),
"HostLogger"
);
assert_eq!(host_contract_name_to_cpp_trait("logger"), "HostLogger");
}
#[test]
fn cpp_type_name_mappings() {
assert_eq!(
cpp_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint32_t"
);
assert_eq!(
cpp_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"StringView"
);
assert_eq!(
cpp_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"Buffer"
);
assert_eq!(
cpp_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"polyplug_generated::MyStruct"
);
}
#[test]
fn generate_cpp_host_contract_trait_produces_class() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![
ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
},
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_cpp_host_contract_trait(&mut out, &contract);
assert!(out.contains("class HostLogger"), "missing class: {out}");
assert!(
out.contains("virtual ~HostLogger() = default"),
"missing virtual destructor: {out}"
);
assert!(
out.contains("virtual void log(StringView message) = 0"),
"missing log method: {out}"
);
assert!(
out.contains("virtual void logf(uint32_t level, StringView format) = 0"),
"missing logf method: {out}"
);
}
#[test]
fn generate_cpp_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_cpp_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(out.contains("class HostLogger"), "missing class: {out}");
assert!(
out.contains("HOSTLOGGER_CONTRACT_ID"),
"missing constant: {out}"
);
assert!(
out.contains("namespace polyplug_host"),
"missing namespace: {out}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_host_contracts_file() {
let generator: CppGenerator = CppGenerator;
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.hpp".to_owned()),
"missing host_contracts.hpp: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_host_contracts_file() {
let generator: CppGenerator = CppGenerator;
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.hpp".to_owned()),
"unexpected host_contracts.hpp: {names:?}"
);
}
#[test]
fn host_contract_name_to_cpp_caller_conversion() {
assert_eq!(
host_contract_name_to_cpp_caller("host.logger"),
"HostLoggerContract"
);
assert_eq!(
host_contract_name_to_cpp_caller("host.fs.reader"),
"HostFsReaderContract"
);
assert_eq!(
host_contract_name_to_cpp_caller("host.HostLogger"),
"HostLoggerContract"
);
assert_eq!(
host_contract_name_to_cpp_caller("logger"),
"HostLoggerContract"
);
}
#[test]
fn cpp_guest_caller_param_type_name_mappings() {
assert_eq!(
cpp_guest_caller_param_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint32_t"
);
assert_eq!(
cpp_guest_caller_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"std::string_view"
);
assert_eq!(
cpp_guest_caller_param_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"Buffer"
);
assert_eq!(
cpp_guest_caller_param_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"const polyplug_generated::MyStruct&"
);
}
#[test]
fn cpp_guest_caller_return_type_name_mappings() {
assert_eq!(
cpp_guest_caller_return_type_name(&ResolvedTypeRef::Primitive(PrimitiveType::U32)),
"uint32_t"
);
assert_eq!(
cpp_guest_caller_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
"std::string_view"
);
assert_eq!(
cpp_guest_caller_return_type_name(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer)),
"std::span<const std::uint8_t>"
);
assert_eq!(
cpp_guest_caller_return_type_name(&ResolvedTypeRef::UserDefined("MyStruct".to_owned())),
"polyplug_generated::MyStruct"
);
}
#[test]
fn cpp_guest_caller_return_expr_is_null_safe() {
let sv_expr: String =
cpp_guest_caller_return_expr(&ResolvedTypeRef::AbiType(AbiBuiltin::StringView));
assert_eq!(sv_expr, "polyplug::to_string_view(out)");
assert!(
!sv_expr.contains("std::string_view(reinterpret_cast"),
"StringView return must not build string_view from raw ptr (UB on null): {sv_expr}"
);
let buf_expr: String =
cpp_guest_caller_return_expr(&ResolvedTypeRef::AbiType(AbiBuiltin::Buffer));
assert!(
buf_expr.contains("out.ptr ?"),
"Buffer return must guard the null pointer before building a span: {buf_expr}"
);
}
#[test]
fn generate_cpp_guest_host_contract_caller_produces_class() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
};
let mut out: String = String::new();
generate_cpp_guest_host_contract_caller(&mut out, &contract);
assert!(
out.contains("class HostLoggerContract"),
"missing class: {out}"
);
assert!(
out.contains("const HostContractInterface* interface_"),
"missing interface member: {out}"
);
assert!(
out.contains("HostContractInstance instance_"),
"missing instance member: {out}"
);
assert!(
out.contains("static std::optional<HostLoggerContract> from_host"),
"missing from_host method: {out}"
);
assert!(
out.contains("void log(std::string_view message) noexcept"),
"missing log method: {out}"
);
assert!(
out.contains("bool is_valid() const noexcept"),
"missing is_valid method: {out}"
);
}
#[test]
fn generate_cpp_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_cpp_guest_host_contracts_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("class HostLoggerContract"),
"missing class: {out}"
);
assert!(
out.contains("HOSTLOGGERCONTRACT_ID"),
"missing constant: {out}"
);
assert!(
out.contains("namespace polyplug_plugin"),
"missing namespace: {out}"
);
}
#[test]
fn generate_guest_with_host_contracts_produces_guest_host_contracts_file() {
let generator: CppGenerator = CppGenerator;
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_contracts.hpp".to_owned()),
"missing guest/host_contracts.hpp: {names:?}"
);
}
#[test]
fn generate_guest_without_host_contracts_no_guest_host_contracts_file() {
let generator: CppGenerator = CppGenerator;
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_contracts.hpp".to_owned()),
"unexpected guest/host_contracts.hpp: {names:?}"
);
}
#[test]
fn generate_host_with_host_contracts_produces_interface_factories_file() {
let generator: CppGenerator = CppGenerator;
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.hpp".to_owned()),
"missing interface_factories.hpp: {names:?}"
);
}
#[test]
fn generate_host_without_host_contracts_no_interface_factories_file() {
let generator: CppGenerator = CppGenerator;
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.hpp".to_owned()),
"unexpected interface_factories.hpp: {names:?}"
);
}
#[test]
fn generate_cpp_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_cpp_host_interface_factories_file(&ir);
assert!(out.contains("AUTO-GENERATED"), "missing header: {out}");
assert!(
out.contains("template<typename T>"),
"missing template: {out}"
);
assert!(
out.contains("create_host_logger_interface"),
"missing NATIVE factory: {out}"
);
assert!(
out.contains("create_host_logger_interface_vm"),
"missing VM factory: {out}"
);
assert!(
out.contains("std::unique_ptr<T> impl"),
"missing unique_ptr param: {out}"
);
assert!(
out.contains("VmDispatch_call_fn dispatch_fn"),
"missing dispatch_fn param: {out}"
);
assert!(
out.contains("namespace polyplug_host"),
"missing namespace: {out}"
);
}
#[test]
fn generate_cpp_host_interface_factory_produces_native_and_vm_factories() {
let contract = ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x1234_5678_9ABC_DEF0_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: false,
functions: vec![ResolvedFunction {
name: "log".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: None,
}],
};
let mut out: String = String::new();
generate_cpp_host_interface_factory(&mut out, &contract);
assert!(
out.contains("template<typename T>"),
"missing template: {out}"
);
assert!(
out.contains("const HostContractInterface* create_host_logger_interface"),
"missing NATIVE factory: {out}"
);
assert!(
out.contains("std::unique_ptr<T> impl"),
"missing unique_ptr: {out}"
);
assert!(
out.contains("T* impl_ptr = impl.release();"),
"implementation must be released into a local, not a static: {out}"
);
assert!(
out.contains("iface->user_data = static_cast<void*>(impl_ptr);"),
"implementation must be routed through user_data, not a static: {out}"
);
assert!(
!out.contains("s_impl"),
"host factory must not hold the implementation in a static: {out}"
);
assert!(
out.contains("auto* iface = new HostContractInterface{"),
"factory must heap-allocate the interface per call: {out}"
);
assert!(
!out.contains("static HostContractInterface s_interface"),
"factory must not hold the interface in a static: {out}"
);
assert!(
out.contains("static void* const FUNCTIONS"),
"missing function array: {out}"
);
assert!(
out.contains("host_logger_log_thunk"),
"missing thunk: {out}"
);
assert!(
out.contains("create_host_logger_interface_vm"),
"missing VM factory: {out}"
);
assert!(
out.contains("VmDispatch_call_fn dispatch_fn"),
"missing dispatch_fn: {out}"
);
assert!(
out.contains("create_instance_stub"),
"missing create_instance stub: {out}"
);
assert!(
out.contains("destroy_instance_stub"),
"missing destroy_instance stub: {out}"
);
}
#[test]
fn peer_caller_emitted_for_declared_dependency() {
let contract_id: u64 = 0xDEAD_BEEF_1234_5678_u64;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.Validator".to_owned(),
contract_id,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "validate".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "input".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
}],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
}],
host_contracts: vec![],
bundle: Some(ResolvedBundle {
name: "cpp_transformer".to_owned(),
bundle_id: 0xAAAA_BBBB_CCCC_DDDD_u64,
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: "transformer".to_owned(),
implements: vec!["data.Transformer@1".to_owned()],
optional: vec![],
}],
dependencies: vec![ResolvedDependency::ByContract {
contract: "pipeline.Validator".to_owned(),
contract_id,
min_version: 1,
}],
needs_reinit_on_dep_reload: false,
}),
};
let generator: CppGenerator = CppGenerator;
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/peer_callers.hpp".to_owned()),
"expected guest/peer_callers.hpp, got: {names:?}"
);
let peer_file: &GeneratedFile = files
.files
.iter()
.find(|f: &&GeneratedFile| f.path.to_string_lossy() == "guest/peer_callers.hpp")
.expect("peer_callers.hpp must be present");
assert!(
peer_file.content.contains("PipelineValidatorContractPeer"),
"expected peer class name in output:\n{}",
peer_file.content
);
assert!(
peer_file.content.contains("switch (iface_->dispatch_type)"),
"peer caller must branch on dispatch type:\n{}",
peer_file.content
);
assert!(
peer_file
.content
.contains("iface_->dispatch.native.functions"),
"peer caller Native arm must index dispatch.native.functions:\n{}",
peer_file.content
);
assert!(
peer_file.content.contains("iface_->dispatch.vm.call"),
"peer caller VM arm must call dispatch.vm.call:\n{}",
peer_file.content
);
assert!(
peer_file.content.contains("find_guest_contract"),
"resolve() must call find_guest_contract:\n{}",
peer_file.content
);
assert!(
peer_file.content.contains("resolve_guest_contract"),
"resolve() must call resolve_guest_contract:\n{}",
peer_file.content
);
assert!(
peer_file
.content
.contains("resolve(const HostApi* host) noexcept"),
"resolve(host) must take the per-instance HostApi pointer:\n{}",
peer_file.content
);
assert!(
peer_file.content.contains("AUTO-GENERATED"),
"missing AUTO-GENERATED header:\n{}",
peer_file.content
);
}
#[test]
fn no_peer_callers_without_dependencies() {
let generator: CppGenerator = CppGenerator;
let ir: ValidatedIr = ValidatedIr {
types: vec![],
enums: vec![],
contracts: vec![ResolvedContract {
name: "pipeline.Validator".to_owned(),
contract_id: 0xDEAD_BEEF_1234_5678_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![],
}],
host_contracts: vec![],
bundle: None,
};
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_guest(&ir, &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.hpp".to_owned()),
"must NOT emit peer_callers.hpp without a bundle: {names:?}"
);
}
fn collision_ir() -> ValidatedIr {
ValidatedIr {
types: vec![crate::ir::ResolvedType {
name: "LogMessage".to_owned(),
fields: vec![crate::ir::ResolvedField {
name: "level".to_owned(),
ty: ResolvedTypeRef::UserDefined("LogLevel".to_owned()),
}],
}],
enums: vec![EnumDef {
name: "LogLevel".to_owned(),
repr: ReprType::U32,
bitflag: false,
variants: vec![
EnumVariant {
name: "Debug".to_owned(),
value: "0".to_owned(),
},
EnumVariant {
name: "Error".to_owned(),
value: "1".to_owned(),
},
],
}],
contracts: vec![ResolvedContract {
name: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
functions: vec![ResolvedFunction {
name: "describe".to_owned(),
function_id: 0,
params: vec![ResolvedParam {
name: "level".to_owned(),
ty: ResolvedTypeRef::UserDefined("LogLevel".to_owned()),
}],
returns: Some(ResolvedTypeRef::AbiType(AbiBuiltin::StringView)),
}],
}],
host_contracts: vec![ResolvedHostContract {
name: "host.logger".to_owned(),
contract_id: 0x5555_6666_7777_8888_u64,
version: Version {
major: 1,
minor: 0,
patch: 0,
},
singleton: true,
functions: vec![ResolvedFunction {
name: "log_with_level".to_owned(),
function_id: 0,
params: vec![
ResolvedParam {
name: "level".to_owned(),
ty: ResolvedTypeRef::UserDefined("LogLevel".to_owned()),
},
ResolvedParam {
name: "message".to_owned(),
ty: ResolvedTypeRef::AbiType(AbiBuiltin::StringView),
},
],
returns: None,
}],
}],
bundle: Some(crate::ir::ResolvedBundle {
name: "decoder_bundle".to_owned(),
bundle_id: 0x9999_AAAA_BBBB_CCCC_u64,
version: 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: "decoder".to_owned(),
implements: vec!["pipeline.decoder@1.0".to_owned()],
optional: vec![],
}],
dependencies: vec![ResolvedDependency::ByContract {
contract: "pipeline.decoder".to_owned(),
contract_id: 0x1111_2222_3333_4444_u64,
min_version: 1,
}],
needs_reinit_on_dep_reload: false,
}),
}
}
fn file_content<'a>(files: &'a GeneratedFiles, name: &str) -> &'a str {
files
.files
.iter()
.find(|f: &&GeneratedFile| f.path.to_string_lossy() == name)
.map(|f: &GeneratedFile| f.content.as_str())
.expect("generated file missing")
}
#[test]
fn generated_files_never_blanket_import_polyplug_generated() {
let generator: CppGenerator = CppGenerator;
let ir: ValidatedIr = collision_ir();
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
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();
for expected in [
"host/types.hpp",
"host/host_callers.hpp",
"host/host_contracts.hpp",
"host/interface_factories.hpp",
"guest/types.hpp",
"guest/contracts.hpp",
"guest/interfaces.hpp",
"guest/init.hpp",
"guest/host_contracts.hpp",
"guest/peer_callers.hpp",
] {
assert!(
names.contains(&expected.to_owned()),
"collision IR must produce {expected}: {names:?}"
);
}
for file in &files.files {
assert!(
!file.content.contains("using namespace polyplug_generated"),
"{} must not blanket-import polyplug_generated:\n{}",
file.path.display(),
file.content
);
}
}
#[test]
fn contract_types_emitted_fully_qualified() {
let generator: CppGenerator = CppGenerator;
let ir: ValidatedIr = collision_ir();
let mut files: GeneratedFiles = GeneratedFiles::default();
generator
.generate_host(&ir, &mut files)
.expect("generate_host");
generator
.generate_guest(&ir, &mut files)
.expect("generate_guest");
let types_hpp: &str = file_content(&files, "host/types.hpp");
assert!(
types_hpp.contains("enum class LogLevel : uint32_t"),
"missing enum definition in types.hpp:\n{types_hpp}"
);
assert!(
types_hpp.contains(" LogLevel level;\n"),
"struct field inside polyplug_generated must stay unqualified:\n{types_hpp}"
);
assert!(
!types_hpp.contains("polyplug_generated::LogLevel"),
"types.hpp must not self-qualify its own definitions:\n{types_hpp}"
);
let host_contracts: &str = file_content(&files, "host/host_contracts.hpp");
assert!(
host_contracts.contains(
"virtual void log_with_level(const polyplug_generated::LogLevel& level, StringView message) = 0;"
),
"host trait method must qualify contract types:\n{host_contracts}"
);
let factories: &str = file_content(&files, "host/interface_factories.hpp");
assert!(
factories.contains("polyplug_generated::LogLevel"),
"host thunk arg extraction must qualify contract types:\n{factories}"
);
let host_callers: &str = file_content(&files, "host/host_callers.hpp");
assert!(
host_callers.contains("polyplug_generated::LogLevel level"),
"host caller params must qualify contract types:\n{host_callers}"
);
let guest_host_contracts: &str = file_content(&files, "guest/host_contracts.hpp");
assert!(
guest_host_contracts.contains("const polyplug_generated::LogLevel& level"),
"guest caller params must qualify contract types:\n{guest_host_contracts}"
);
assert!(
guest_host_contracts.contains(" polyplug_generated::LogLevel level;"),
"guest caller packed-args fields must qualify contract types:\n{guest_host_contracts}"
);
let contracts_hpp: &str = file_content(&files, "guest/contracts.hpp");
assert!(
contracts_hpp.contains("const polyplug_generated::LogLevel& level"),
"guest abstract method params must qualify contract types:\n{contracts_hpp}"
);
let interfaces_hpp: &str = file_content(&files, "guest/interfaces.hpp");
assert!(
interfaces_hpp.contains("static_cast<const polyplug_generated::LogLevel*>(args)"),
"guest ABI wrapper casts must qualify contract types:\n{interfaces_hpp}"
);
let peer_callers: &str = file_content(&files, "guest/peer_callers.hpp");
assert!(
peer_callers.contains("polyplug_generated::LogLevel level"),
"peer caller params must qualify contract types:\n{peer_callers}"
);
let atomic_idiom: &str = "reinterpret_cast<const std::atomic<std::uint64_t>*>(revision_ptr)->load(std::memory_order_acquire)";
assert!(
host_callers.contains("#include <atomic>") && host_callers.contains(atomic_idiom),
"host_callers.hpp must include <atomic> and emit the acquire-load idiom:\n{host_callers}"
);
assert!(
peer_callers.contains("#include <atomic>") && peer_callers.contains(atomic_idiom),
"peer_callers.hpp must include <atomic> and emit the acquire-load idiom:\n{peer_callers}"
);
assert!(
peer_callers.contains(
"if (polyplug_load_revision(revision_ptr_) != cached_revision_ && !revalidate())"
),
"peer dispatch must re-resolve before use when the revision changed:\n{peer_callers}"
);
assert!(
peer_callers.contains("bool revalidate() noexcept"),
"peer caller must have a revalidate() helper:\n{peer_callers}"
);
}
}