#![allow(clippy::std_instead_of_core)]
use crate::mapper::map_all_abi_types;
use crate::types::AbiTypes;
use polyplug_codegen::data::Item;
use polyplug_codegen::languages::{
CSharpGenerator, CodeGenerator, CppGenerator, ForwardKind, GenerationContext, JsGenerator,
LuaGenerator, PythonGenerator,
};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetLang {
Cpp,
CSharp,
Python,
Lua,
JavaScript,
}
impl TargetLang {
pub const fn language_name(&self) -> &'static str {
match self {
TargetLang::Cpp => "cpp",
TargetLang::CSharp => "csharp",
TargetLang::Python => "python",
TargetLang::Lua => "lua",
TargetLang::JavaScript => "js",
}
}
pub const fn output_filename(&self) -> &'static str {
match self {
TargetLang::Cpp => "abi.hpp",
TargetLang::CSharp => "Abi.cs",
TargetLang::Python => "abi.py",
TargetLang::Lua => "abi.lua",
TargetLang::JavaScript => "abi.ts",
}
}
pub const fn subdir(&self) -> &'static str {
match self {
TargetLang::Cpp => "polyplug",
TargetLang::CSharp => "",
TargetLang::Python => "",
TargetLang::Lua => "",
TargetLang::JavaScript => "",
}
}
}
const UNREPRESENTABLE_PATTERNS: &[&str] = &["dyn ", "impl ", "for<", "where "];
const HELPER_CSHARP_STRING_VIEW: &str = r#"
/// <summary>
/// Helpers for constructing and converting StringViews at the ABI boundary.
/// This is the unified implementation used by both host and guest.
/// </summary>
public static class StringViewHelper
{
/// <summary>
/// Returns a StringView pointing at the pinned byte array via a GCHandle.
/// Caller owns the GCHandle and must keep it alive while the StringView is in use.
/// </summary>
public static StringView FromPinnedHandle(GCHandle handle, int length) =>
new StringView { Ptr = handle.AddrOfPinnedObject(), Len = (nuint)length };
/// <summary>
/// Returns a StringView pointing at a pre-pinned IntPtr. Caller ensures ptr validity.
/// </summary>
public static StringView FromPtr(IntPtr ptr, int length) =>
new StringView { Ptr = ptr, Len = (nuint)length };
/// <summary>
/// Creates a StringView from a .NET string by pinning it in memory.
/// The GCHandle must be kept alive while the StringView is in use.
/// For guest plugins, return strings should use host allocation via registrar.
/// </summary>
public static (StringView View, GCHandle Handle) FromStringPinned(string str)
{
if (string.IsNullOrEmpty(str))
return (new StringView { Ptr = IntPtr.Zero, Len = 0 }, default);
byte[] bytes = Encoding.UTF8.GetBytes(str);
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
StringView sv = new StringView { Ptr = handle.AddrOfPinnedObject(), Len = (nuint)bytes.Length };
return (sv, handle);
}
/// <summary>
/// Converts a StringView to a .NET string by decoding the UTF-8 bytes
/// directly from the native pointer (no intermediate byte[] copy).
/// A null/zero-length view decodes to the empty string. A non-null view
/// whose bytes are NOT valid UTF-8 throws <see cref="DecoderFallbackException"/>
/// — the helper never silently substitutes replacement characters for a
/// readable-but-invalid view.
/// </summary>
public static unsafe string ToString(this StringView sv)
{
if (sv.Ptr == IntPtr.Zero || sv.Len == 0)
return string.Empty;
return s_strictUtf8.GetString((byte*)sv.Ptr, (int)sv.Len);
}
/// <summary>
/// Converts a StringView to a .NET string. Alias for ToString.
/// </summary>
public static string ToStr(StringView sv) => ToString(sv);
/// <summary>
/// Checks if a StringView starts with the given prefix.
/// </summary>
public static bool StartsWith(StringView sv, string prefix)
{
if (string.IsNullOrEmpty(prefix))
return true;
if (sv.Ptr == IntPtr.Zero || sv.Len == 0)
return false;
string str = ToString(sv);
return str.StartsWith(prefix);
}
/// <summary>
/// Checks if a StringView ends with the given suffix.
/// </summary>
public static bool EndsWith(StringView sv, string suffix)
{
if (string.IsNullOrEmpty(suffix))
return true;
if (sv.Ptr == IntPtr.Zero || sv.Len == 0)
return false;
string str = ToString(sv);
return str.EndsWith(suffix);
}
/// <summary>
/// Strips the prefix from a StringView if it starts with it.
/// Returns the original string if the prefix is not present.
/// </summary>
public static string StripPrefix(StringView sv, string prefix)
{
if (string.IsNullOrEmpty(prefix))
return ToString(sv);
string str = ToString(sv);
if (str.StartsWith(prefix))
return str.Substring(prefix.Length);
return str;
}
/// <summary>
/// Splits a StringView by the given delimiter and returns an array of strings.
/// </summary>
public static string[] Split(StringView sv, string delimiter)
{
if (sv.Ptr == IntPtr.Zero || sv.Len == 0)
return System.Array.Empty<string>();
string str = ToString(sv);
if (string.IsNullOrEmpty(delimiter))
return new[] { str };
return str.Split(new[] { delimiter }, System.StringSplitOptions.None);
}
/// <summary>
/// Returns a process-lifetime <see cref="StringView"/> for a string that
/// crosses the ABI boundary as an <see cref="AbiError.Message"/>.
/// </summary>
/// <remarks>
/// Per the ABI ownership contract, an AbiError.Message is a static or
/// runtime-owned string that the receiver MUST NEVER free. This helper pins
/// the UTF-8 bytes for the lifetime of the process (equivalent to .rodata),
/// caching one buffer per distinct string so repeated errors never leak a new
/// GCHandle. It is the only sound way to hand the host a borrowed message that
/// nobody frees. Use it for fixed error literals — NOT for per-call argument
/// strings (use <see cref="PinnedUtf8"/> for those).
/// </remarks>
public static StringView StaticMessage(string value)
{
if (string.IsNullOrEmpty(value))
return new StringView { Ptr = IntPtr.Zero, Len = 0 };
lock (s_staticMessages)
{
if (s_staticMessages.TryGetValue(value, out StringView cached))
return cached;
byte[] bytes = Encoding.UTF8.GetBytes(value);
// Never freed: pinned for the process lifetime, mirroring .rodata.
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
StringView view = new StringView
{
Ptr = handle.AddrOfPinnedObject(),
Len = (nuint)bytes.Length,
};
s_staticMessages[value] = view;
return view;
}
}
private static readonly System.Collections.Generic.Dictionary<string, StringView> s_staticMessages =
new System.Collections.Generic.Dictionary<string, StringView>();
// Strict UTF-8 decoder: throws DecoderFallbackException on invalid bytes
// instead of emitting U+FFFD replacement characters. Every StringView decode
// (ToString and the helpers built on it) routes through this so a
// readable-but-invalid view is surfaced as an error, never silently mangled.
private static readonly UTF8Encoding s_strictUtf8 =
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
}
/// <summary>
/// Call-scoped pin of a .NET string's UTF-8 bytes, exposing a borrowed
/// <see cref="StringView"/> for passing a string ARGUMENT across the ABI
/// boundary. The argument is borrowed for the duration of the call only:
/// construct a <see cref="PinnedUtf8"/>, pass <see cref="View"/> into the call,
/// then dispose to release the pin.
/// </summary>
/// <remarks>
/// This is the correct mechanism for argument strings. It pins managed bytes
/// only for the call and frees the <see cref="GCHandle"/> on <see cref="Dispose"/>,
/// so there is no per-call leak. The host never frees this memory — it only reads
/// it for the duration of the call. For AbiError.Message values, use
/// <see cref="StringViewHelper.StaticMessage"/> instead.
/// </remarks>
public sealed class PinnedUtf8 : IDisposable
{
private GCHandle _handle;
private bool _pinned;
/// <summary>
/// The borrowed <see cref="StringView"/> over the pinned UTF-8 bytes. Valid
/// only until <see cref="Dispose"/> is called.
/// </summary>
public StringView View { get; }
public PinnedUtf8(string value)
{
if (string.IsNullOrEmpty(value))
{
View = new StringView { Ptr = IntPtr.Zero, Len = 0 };
return;
}
byte[] bytes = Encoding.UTF8.GetBytes(value);
_handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
_pinned = true;
View = new StringView
{
Ptr = _handle.AddrOfPinnedObject(),
Len = (nuint)bytes.Length,
};
}
public void Dispose()
{
if (_pinned)
{
_handle.Free();
_pinned = false;
}
}
}
"#;
const HELPER_LUA_HASHING: &str = r#"
--- Compute FNV-1a 64-bit hash of a Lua string.
-- @param str string Input string.
-- @return cdata uint64_t FNV-1a 64-bit hash.
function M.fnv1a_64(str)
local bit = require("bit")
local h = 0xcbf29ce484222325ULL
for i = 1, #str do
h = bit.bxor(h, str:byte(i))
h = h * 0x00000100000001B3ULL
end
return h
end
--- Compute a bundle ID from its name using FNV-1a 64-bit hash.
-- @param name string Bundle name.
-- @return cdata uint64_t Bundle ID hash.
function M.bundle_id(name)
return M.fnv1a_64(name)
end
--- Calculate guest contract ID from name and major version.
-- @param name string Contract name.
-- @param major_version number Major version.
-- @return cdata uint64_t Guest contract ID hash.
function M.guest_contract_id(name, major_version)
return M.fnv1a_64("guest_contract:" .. name .. "@" .. tostring(major_version))
end
--- Calculate host contract ID from name and major version.
-- @param name string Contract name.
-- @param major_version number Major version.
-- @return cdata uint64_t Host contract ID hash.
function M.host_contract_id(name, major_version)
return M.fnv1a_64("host_contract:" .. name .. "@" .. tostring(major_version))
end
"#;
const HELPER_PYTHON_HASHING: &str = r#"
# ─── FNV-1a 64-bit Hash / Contract-ID Helpers ─────────────────────────────────
FNV_OFFSET: int = 0xCBF29CE484222325
FNV_PRIME: int = 0x00000100000001B3
def fnv1a_64(data: bytes) -> int:
"""Compute FNV-1a 64-bit hash of a byte sequence."""
hash_val: int = FNV_OFFSET
for byte in data:
hash_val ^= byte
hash_val = (hash_val * FNV_PRIME) & 0xFFFFFFFFFFFFFFFF
return hash_val
def guest_contract_id(name: str, major_version: int) -> int:
"""Calculate guest contract ID from name and major version."""
return fnv1a_64(f"guest_contract:{name}@{major_version}".encode("utf-8"))
def host_contract_id(name: str, major_version: int) -> int:
"""Calculate host contract ID from name and major version."""
return fnv1a_64(f"host_contract:{name}@{major_version}".encode("utf-8"))
def bundle_id(name: str) -> int:
"""Compute a bundle ID from its name using FNV-1a 64-bit hash."""
return fnv1a_64(name.encode("utf-8"))
"#;
const HELPER_CSHARP_HASHING: &str = r#"
/// <summary>
/// FNV-1a 64-bit hashing and contract/bundle ID computation helpers.
/// Mirrors the canonical scheme in crates/polyplug_utils.
/// </summary>
public static class ContractId
{
private const ulong FnvOffset = 0xCBF29CE484222325UL;
private const ulong FnvPrime = 0x00000100000001B3UL;
/// <summary>Compute the FNV-1a 64-bit hash of the UTF-8 bytes of <paramref name="data"/>.</summary>
public static ulong Fnv1a64(string data)
{
ulong hash = FnvOffset;
foreach (byte b in System.Text.Encoding.UTF8.GetBytes(data))
{
hash ^= b;
hash *= FnvPrime;
}
return hash;
}
/// <summary>Compute a bundle ID from its name using FNV-1a 64-bit.</summary>
public static ulong BundleId(string name) => Fnv1a64(name);
/// <summary>Compute a guest contract ID from name and major version.</summary>
public static ulong GuestContractId(string name, uint majorVersion) =>
Fnv1a64($"guest_contract:{name}@{majorVersion}");
/// <summary>Compute a host contract ID from name and major version.</summary>
public static ulong HostContractId(string name, uint majorVersion) =>
Fnv1a64($"host_contract:{name}@{majorVersion}");
}
"#;
const HELPER_LUA: &str = r#"
--- Convert StringView to Lua string.
-- @param sv StringView from polyplug ABI (ffi.cdata), or nil for a null view
-- @return string Lua string (UTF-8), empty string if nil/empty
-- Raises if given anything other than a StringView cdata or nil — most often a
-- Lua string that was already converted (double-conversion), which would
-- otherwise silently yield "" because a Lua string has no `.ptr` field.
-- Raises if the viewed bytes are not valid UTF-8: LuaJIT's ffi.string copies
-- raw bytes without checking, so a readable-but-invalid view is validated here
-- (a manual scan — LuaJIT 5.1 has no utf8 library) and surfaced as an error
-- rather than silently yielding mojibake. The validation is inline because the
-- Lua helper extractor only preserves `function M.*` blocks (no module locals).
function M.to_str(sv)
if sv == nil then
return ""
end
if type(sv) ~= "cdata" then
error("polyplug.to_str: expected a StringView cdata (or nil), got a " ..
type(sv) .. " — did you already convert it to a Lua string? " ..
"Pass the original StringView, not its to_str() result.", 2)
end
if sv.ptr == nil or sv.len == 0 then
return ""
end
local s = ffi.string(sv.ptr, sv.len)
local i, n = 1, #s
while i <= n do
local c = string.byte(s, i)
if c < 0x80 then
i = i + 1
else
local extra, min_cp, cp
if c >= 0xC0 and c < 0xE0 then
extra, min_cp, cp = 1, 0x80, c % 0x20
elseif c >= 0xE0 and c < 0xF0 then
extra, min_cp, cp = 2, 0x800, c % 0x10
elseif c >= 0xF0 and c < 0xF8 then
extra, min_cp, cp = 3, 0x10000, c % 0x08
else
error("polyplug.to_str: StringView contains invalid UTF-8 " ..
"(invalid lead byte)", 2)
end
if i + extra > n then
error("polyplug.to_str: StringView contains invalid UTF-8 " ..
"(truncated sequence)", 2)
end
for k = 1, extra do
local cc = string.byte(s, i + k)
if cc < 0x80 or cc >= 0xC0 then
error("polyplug.to_str: StringView contains invalid UTF-8 " ..
"(bad continuation byte)", 2)
end
cp = cp * 0x40 + (cc % 0x40)
end
if cp < min_cp or cp > 0x10FFFF or (cp >= 0xD800 and cp <= 0xDFFF) then
error("polyplug.to_str: StringView contains invalid UTF-8 " ..
"(overlong, surrogate, or out-of-range code point)", 2)
end
i = i + extra + 1
end
end
return s
end
--- Check if StringView starts with prefix.
-- @param sv StringView from polyplug ABI
-- @param prefix string Prefix string to check for
-- @return boolean True if the string starts with the prefix
function M.starts_with(sv, prefix)
local s = M.to_str(sv)
return s:sub(1, #prefix) == prefix
end
--- Check if StringView ends with suffix.
-- @param sv StringView from polyplug ABI
-- @param suffix string Suffix string to check for
-- @return boolean True if the string ends with the suffix
function M.ends_with(sv, suffix)
local s = M.to_str(sv)
if #suffix > #s then
return false
end
return s:sub(-#suffix) == suffix
end
--- Strip prefix from StringView if present.
-- @param sv StringView from polyplug ABI
-- @param prefix string Prefix string to strip
-- @return string String with prefix removed if present, otherwise original
function M.strip_prefix(sv, prefix)
local s = M.to_str(sv)
if s:sub(1, #prefix) == prefix then
return s:sub(#prefix + 1)
end
return s
end
--- Split StringView by a literal delimiter, keeping empty segments.
-- The delimiter is matched literally (plain find), never as a Lua pattern.
-- @param sv StringView from polyplug ABI
-- @param delimiter string Literal delimiter string to split by
-- @return table {} for a nil/empty view, { s } for a nil/empty delimiter,
-- otherwise the segments around every occurrence (empties kept)
function M.split(sv, delimiter)
local s = M.to_str(sv)
if s == "" then
return {}
end
if delimiter == nil or delimiter == "" then
return { s }
end
local result = {}
local start = 1
while true do
local i, j = string.find(s, delimiter, start, true)
if i == nil then
table.insert(result, s:sub(start))
break
end
table.insert(result, s:sub(start, i - 1))
start = j + 1
end
return result
end
"#;
const HELPER_JS: &str = r#"
/**
* Convert a StringView to a JavaScript string.
*
* Reads the viewed bytes through Deno's FFI pointer view (this abi mirror runs
* in the Deno host environment) and decodes them as UTF-8.
* @param sv - The StringView to convert.
* @returns The decoded string, or empty string for a null/zero-length view.
* @throws Error when no Deno FFI environment is available — never silently
* returns '' for a readable view.
* @throws TypeError when the viewed bytes are not valid UTF-8 — the fatal
* decoder never substitutes U+FFFD for a readable-but-invalid view.
*/
export function stringViewToString(sv: StringView | null | undefined): string {
if (!sv || sv.ptr === 0n || sv.len === 0) return '';
const deno = (globalThis as {
Deno?: {
UnsafePointer: { create(value: bigint): unknown };
UnsafePointerView: new (pointer: unknown) => {
getArrayBuffer(byteLength: number): ArrayBuffer;
};
};
}).Deno;
if (deno === undefined) {
throw new Error(
'stringViewToString: no Deno FFI environment is available to read StringView memory ' +
'(Deno.UnsafePointerView is required; refusing to silently return an empty string)',
);
}
const pointer: unknown = deno.UnsafePointer.create(sv.ptr);
if (pointer === null) return '';
const bytes: ArrayBuffer = new deno.UnsafePointerView(pointer).getArrayBuffer(Number(sv.len));
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
}
/**
* Strip a prefix from a string.
* @param sv - The input StringView or string.
* @param prefix - The prefix to strip.
* @returns The string without prefix, or original if prefix not present.
*/
export function stripPrefix(sv: StringView | string, prefix: string): string {
const s: string = typeof sv === 'string' ? sv : stringViewToString(sv);
if (s.startsWith(prefix)) {
return s.slice(prefix.length);
}
return s;
}
/**
* Check if a string starts with a prefix.
* @param sv - The input StringView or string.
* @param prefix - The prefix to check.
* @returns True if the string starts with the prefix.
*/
export function startsWith(sv: StringView | string, prefix: string): boolean {
const s: string = typeof sv === 'string' ? sv : stringViewToString(sv);
return s.startsWith(prefix);
}
/**
* Check if a string ends with a suffix.
* @param sv - The input StringView or string.
* @param suffix - The suffix to check.
* @returns True if the string ends with the suffix.
*/
export function endsWith(sv: StringView | string, suffix: string): boolean {
const s: string = typeof sv === 'string' ? sv : stringViewToString(sv);
return s.endsWith(suffix);
}
/**
* Convert a StringView to a JavaScript string (shorthand alias).
* @param sv - The StringView to convert.
* @returns The JavaScript string, or empty string if null/empty.
*/
export function toStr(sv: StringView | null | undefined): string {
return stringViewToString(sv);
}
/**
* Split a string by a literal delimiter, keeping empty segments.
* @param sv - The input StringView or string.
* @param delimiter - The literal delimiter to split by.
* @returns An array of strings: [] for a null/empty input, [s] for an empty delimiter.
*/
export function split(sv: StringView | string, delimiter: string): string[] {
const s: string = typeof sv === 'string' ? sv : stringViewToString(sv);
if (s.length === 0) return [];
if (delimiter.length === 0) return [s];
return s.split(delimiter);
}
/** FNV-1a 64-bit offset basis (matches polyplug_utils::fnv1a_64). */
const FNV_OFFSET_BASIS_64: bigint = 0xcbf29ce484222325n;
/** FNV-1a 64-bit prime (matches polyplug_utils::fnv1a_64). */
const FNV_PRIME_64: bigint = 0x00000100000001b3n;
/** 64-bit wrap mask applied after every FNV-1a round. */
const U64_WRAP_MASK: bigint = 0xffffffffffffffffn;
/** Shared UTF-8 encoder for hashing string inputs. */
const ID_TEXT_ENCODER: TextEncoder = new TextEncoder();
/**
* Compute the FNV-1a 64-bit hash of UTF-8 bytes or a string.
*
* This is the canonical ID primitive mirrored from `polyplug_utils::fnv1a_64`;
* `bundleId`, `guestContractId`, and `hostContractId` are all derived from it so
* every language computes byte-identical identifiers.
* @param data - The bytes to hash, or a string encoded as UTF-8 first.
* @returns The 64-bit hash as a bigint.
*/
export function fnv1a64(data: Uint8Array | string): bigint {
const bytes: Uint8Array = typeof data === 'string' ? ID_TEXT_ENCODER.encode(data) : data;
let h: bigint = FNV_OFFSET_BASIS_64;
for (const b of bytes) {
h = (h ^ BigInt(b)) * FNV_PRIME_64;
h = h & U64_WRAP_MASK;
}
return h;
}
/**
* Compute a guest contract ID (matches polyplug_utils::guest_contract_id).
*
* Guest contract IDs use a distinct prefix to avoid collisions with host contracts.
* @param name - Contract name (e.g., "pipeline.Decoder").
* @param majorVersion - Major version number.
* @returns The 64-bit contract ID as a bigint.
*/
export function guestContractId(name: string, majorVersion: number): bigint {
return fnv1a64(`guest_contract:${name}@${majorVersion}`);
}
/**
* Compute a host contract ID (matches polyplug_utils::host_contract_id).
*
* Host contract IDs use a distinct prefix to avoid collisions with guest contracts.
* @param name - Host contract name (must start with "host.", e.g., "host.logger").
* @param majorVersion - Major version number.
* @returns The 64-bit host contract ID as a bigint.
*/
export function hostContractId(name: string, majorVersion: number): bigint {
return fnv1a64(`host_contract:${name}@${majorVersion}`);
}
/**
* Compute a bundle ID (matches polyplug_utils::bundle_id).
* @param name - Bundle name.
* @returns The 64-bit bundle ID as a bigint.
*/
export function bundleId(name: string): bigint {
return fnv1a64(name);
}
"#;
const HELPER_CPP: &str = r#"
namespace polyplug {
namespace abi {
/// Convert StringView to std::string_view (zero-copy).
/// This is the raw byte primitive: it does NOT validate UTF-8 (it is the
/// explicit escape hatch for callers that knowingly want the bytes). Helpers
/// that decode — to_string / to_str / starts_with / ends_with / strip_prefix /
/// split — validate and throw on invalid UTF-8.
inline std::string_view to_string_view(StringView sv) noexcept {
if (!sv.ptr || sv.len == 0) return {};
return {reinterpret_cast<const char*>(sv.ptr), sv.len};
}
/// Returns true if every byte of `s` is part of a well-formed UTF-8 sequence
/// (rejecting overlong forms, surrogates, and code points above U+10FFFF) —
/// matching Rust's core::str::from_utf8 strictness. C++ has no standard UTF-8
/// validator, so this scan is the cross-language floor.
inline bool is_valid_utf8(std::string_view s) noexcept {
size_t i = 0, n = s.size();
while (i < n) {
unsigned char c = static_cast<unsigned char>(s[i]);
if (c < 0x80) { i++; continue; }
size_t extra;
unsigned int min_cp, cp;
if ((c & 0xE0) == 0xC0) { extra = 1; min_cp = 0x80; cp = c & 0x1F; }
else if ((c & 0xF0) == 0xE0) { extra = 2; min_cp = 0x800; cp = c & 0x0F; }
else if ((c & 0xF8) == 0xF0) { extra = 3; min_cp = 0x10000; cp = c & 0x07; }
else { return false; }
if (i + extra >= n) return false;
for (size_t k = 1; k <= extra; k++) {
unsigned char cc = static_cast<unsigned char>(s[i + k]);
if ((cc & 0xC0) != 0x80) return false;
cp = (cp << 6) | (cc & 0x3F);
}
if (cp < min_cp || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return false;
i += extra + 1;
}
return true;
}
/// Throws std::runtime_error if `s` is not valid UTF-8. Used by every decoding
/// helper so a readable-but-invalid view surfaces an error instead of mojibake.
inline void require_utf8(std::string_view s) {
if (!is_valid_utf8(s)) {
throw std::runtime_error("polyplug: StringView contains invalid UTF-8");
}
}
/// Convert StringView to std::string (copies data).
/// Throws std::runtime_error if the viewed bytes are not valid UTF-8.
inline std::string to_string(StringView sv) {
if (!sv.ptr || sv.len == 0) return {};
std::string_view s = to_string_view(sv);
require_utf8(s);
return {s.data(), s.size()};
}
/// Convert StringView to std::string (alias for to_string).
/// Throws std::runtime_error on invalid UTF-8.
inline std::string to_str(StringView sv) {
return to_string(sv);
}
/// Strip prefix from a string.
/// @param sv The input StringView.
/// @param prefix The prefix to strip.
/// @return std::string_view without prefix if it starts with prefix, otherwise original.
inline std::string_view strip_prefix(StringView sv, std::string_view prefix) {
auto s = to_string_view(sv);
require_utf8(s);
if (s.size() >= prefix.size() && s.substr(0, prefix.size()) == prefix) {
return s.substr(prefix.size());
}
return s;
}
/// Check if string starts with prefix.
/// @param sv The input StringView.
/// @param prefix The prefix to check.
/// @return true if string starts with prefix.
inline bool starts_with(StringView sv, std::string_view prefix) {
auto s = to_string_view(sv);
require_utf8(s);
return s.size() >= prefix.size() && s.substr(0, prefix.size()) == prefix;
}
/// Check if string ends with suffix.
/// @param sv The input StringView.
/// @param suffix The suffix to check.
/// @return true if string ends with suffix.
inline bool ends_with(StringView sv, std::string_view suffix) {
auto s = to_string_view(sv);
require_utf8(s);
if (s.size() < suffix.size()) return false;
return s.substr(s.size() - suffix.size()) == suffix;
}
/// Split string by a literal delimiter, keeping empty segments.
/// @param sv The input StringView.
/// @param delimiter The literal delimiter string.
/// @return Vector of string_views: {} for a null/empty view, {s} for an empty
/// delimiter, otherwise the segments around every occurrence (empties kept).
inline std::vector<std::string_view> split(StringView sv, std::string_view delimiter) {
auto s = to_string_view(sv);
require_utf8(s);
std::vector<std::string_view> result;
if (s.empty()) {
return result;
}
if (delimiter.empty()) {
result.push_back(s);
return result;
}
size_t start = 0;
while (true) {
size_t pos = s.find(delimiter, start);
if (pos == std::string_view::npos) {
result.push_back(s.substr(start));
break;
}
result.push_back(s.substr(start, pos - start));
start = pos + delimiter.size();
}
return result;
}
/// Create StringView from string literal (borrowed)
inline StringView string_view(const char* s) noexcept {
return {reinterpret_cast<const uint8_t*>(s), std::strlen(s)};
}
/// Create StringView from std::string (borrowed - ensure string outlives view)
inline StringView string_view(const std::string& s) noexcept {
return {reinterpret_cast<const uint8_t*>(s.data()), s.size()};
}
/// Create StringView from std::string_view (borrowed)
inline StringView string_view(std::string_view s) noexcept {
return {reinterpret_cast<const uint8_t*>(s.data()), s.size()};
}
// NOTE: cross-boundary allocation (alloc_string) lives in the guest SDK
// (polyplug::alloc_string in guest.hpp), which routes through the stored
// HostApi. abi.hpp stays pure ABI with no link-time host dependency.
} // namespace abi
} // namespace polyplug
"#;
const KNOWN_SIZES: &[(&str, usize)] = &[
("StringView", 16),
("Buffer", 24),
("CallArena", 40),
("ArenaOverflowBlock", 24),
("Version", 12),
("AbiError", 24),
("DependencyInfo", 24),
("DispatchMechanisms", 16),
("GuestContractInterface", 56),
("GuestContractInstance", 16),
("HostApi", 184),
("HostContractInterface", 80),
("HostContractInstance", 8),
("GuestContractHandle", 8),
("PluginDescriptor", 48),
("BundleInitContext", 24),
("RuntimeConfig", 48),
("ReloadPhase", 48),
("NativeDispatch", 16),
("VmDispatch", 16),
("VmLoaderData", 8),
];
fn populate_size_hints(abi_types: &mut AbiTypes) {
for struct_info in &mut abi_types.structs {
if struct_info.size_hint.is_none() {
for (name, size) in KNOWN_SIZES {
if struct_info.name == *name {
struct_info.size_hint = Some(*size);
break;
}
}
}
}
}
fn validate_representable_types(abi_types: &AbiTypes) -> Result<(), String> {
for struct_info in &abi_types.structs {
for field in &struct_info.fields {
for pattern in UNREPRESENTABLE_PATTERNS {
if field.rust_type.contains(pattern) {
return Err(format!(
"Cannot represent type '{}' field '{}' with type '{}' in target languages. \
Consider simplifying the type or adding codegen support.",
struct_info.name, field.name, field.rust_type
));
}
}
}
}
Ok(())
}
fn generate_auto_header(lang: TargetLang) -> String {
match lang {
TargetLang::Python => [
"# THIS FILE IS AUTO-GENERATED BY polyplug_abi build script.",
"# DO NOT EDIT STRUCT/FIELD DEFINITIONS.",
"# Helper methods are embedded by the build script (see crates/polyplug_abi/build/generate.rs).",
"",
]
.join("\n"),
TargetLang::CSharp => [
"// THIS FILE IS AUTO-GENERATED BY polyplug_abi build script.",
"// DO NOT EDIT STRUCT/FIELD DEFINITIONS.",
"// Helper methods are embedded by the build script (see crates/polyplug_abi/build/generate.rs).",
"",
]
.join("\n"),
TargetLang::Lua => [
"-- THIS FILE IS AUTO-GENERATED BY polyplug_abi build script.",
"-- DO NOT EDIT STRUCT/FIELD DEFINITIONS.",
"-- Helper methods are embedded by the build script (see crates/polyplug_abi/build/generate.rs).",
"",
]
.join("\n"),
TargetLang::JavaScript => [
"// THIS FILE IS AUTO-GENERATED BY polyplug_abi build script.",
"// DO NOT EDIT STRUCT/FIELD DEFINITIONS.",
"// Helper methods are embedded by the build script (see crates/polyplug_abi/build/generate.rs).",
"",
]
.join("\n"),
TargetLang::Cpp => [
"// THIS FILE IS AUTO-GENERATED BY polyplug_abi build script.",
"// DO NOT EDIT STRUCT/FIELD DEFINITIONS.",
"// Helper methods are embedded by the build script (see crates/polyplug_abi/build/generate.rs).",
"",
]
.join("\n"),
}
}
pub fn generate_language_sdk(lang: TargetLang, abi_types: &AbiTypes) -> String {
let all_items: Vec<Item> = map_all_abi_types(&abi_types.types());
let generator: Box<dyn CodeGenerator> = match lang {
TargetLang::Cpp => Box::new(CppGenerator::new()),
TargetLang::CSharp => Box::new(CSharpGenerator::new()),
TargetLang::Python => Box::new(PythonGenerator::new()),
TargetLang::Lua => Box::new(LuaGenerator::new()),
TargetLang::JavaScript => Box::new(JsGenerator::new()),
};
let enum_reprs: std::collections::HashMap<String, String> = all_items
.iter()
.filter_map(|item| match item {
Item::Enum(e) => Some((e.name.clone(), e.repr.clone())),
_ => None,
})
.collect();
let ctx: GenerationContext = GenerationContext::new().with_enum_reprs(enum_reprs);
let mut output: String = String::new();
output.push_str(&generate_auto_header(lang));
output.push_str(&generator.generate_header(&ctx));
let emit_items: Vec<Item> = match lang {
TargetLang::Cpp => {
output.push_str(&cpp_forward_declarations(&all_items));
cpp_dependency_ordered(all_items)
}
TargetLang::Python => python_dependency_ordered(all_items),
TargetLang::Lua => {
output.push_str(&lua_forward_declarations(&all_items));
lua_dependency_ordered(all_items)
}
_ => all_items,
};
let lua_consts: &mut Vec<&Item> = &mut Vec::new();
for item in &emit_items {
if lang == TargetLang::Lua && matches!(item, Item::Const(_)) {
lua_consts.push(item);
continue;
}
let code: String = match item {
Item::Const(c) => generator.generate_const(c, &ctx),
Item::Struct(s) => generator.generate_struct(s, &ctx),
Item::Enum(e) => generator.generate_enum(e, &ctx),
Item::Union(u) => generator.generate_union(u, &ctx),
Item::Function(_) => String::new(),
};
output.push_str(&code);
}
if lang == TargetLang::Lua {
output.push_str("]]\n\n");
for item in lua_consts.iter() {
if let Item::Const(c) = item {
output.push_str(&generator.generate_const(c, &ctx));
}
}
output.push('\n');
}
output.push_str(&generator.generate_footer(&ctx));
output
}
fn cpp_forward_declarations(items: &[Item]) -> String {
let mut output = String::from("// ─── Forward declarations ───\n");
for item in items {
let decl = match item {
Item::Struct(s) => CppGenerator::forward_declaration(&s.name, ForwardKind::Struct),
Item::Union(u) => CppGenerator::forward_declaration(&u.name, ForwardKind::Union),
Item::Enum(e) => {
CppGenerator::forward_declaration(&e.name, ForwardKind::Enum(e.repr.clone()))
}
Item::Const(_) | Item::Function(_) => continue,
};
output.push_str(&decl);
}
output.push('\n');
output
}
fn lua_forward_declarations(items: &[Item]) -> String {
let mut output = String::from(" // ─── Forward declarations ───\n");
for item in items {
let decl: String = match item {
Item::Struct(s) => format!(" typedef struct {} {};\n", s.name, s.name),
Item::Union(u) => format!(" typedef union {} {};\n", u.name, u.name),
Item::Enum(e) => format!(" typedef enum {} {};\n", e.name, e.name),
Item::Const(_) | Item::Function(_) => continue,
};
output.push_str(&decl);
}
output.push('\n');
output
}
fn lua_dependency_ordered(items: Vec<Item>) -> Vec<Item> {
use std::collections::HashSet;
let aggregate_names: HashSet<String> = items
.iter()
.filter_map(|item| match item {
Item::Struct(s) => Some(s.name.clone()),
Item::Union(u) => Some(u.name.clone()),
Item::Enum(e) => Some(e.name.clone()),
_ => None,
})
.collect();
let dependencies = |item: &Item| -> Vec<String> {
let field_types: Vec<&str> = match item {
Item::Struct(s) => s.fields.iter().map(|f| f.rust_type.as_str()).collect(),
Item::Union(u) => u.variants.iter().map(|v| v.type_name.as_str()).collect(),
_ => Vec::new(),
};
field_types
.iter()
.filter_map(|rust_type| LuaGenerator::value_dependency(rust_type))
.filter(|dep| aggregate_names.contains(dep))
.collect()
};
let mut ordered: Vec<Item> = Vec::with_capacity(items.len());
let mut emitted: HashSet<String> = HashSet::new();
let mut pending: Vec<Item> = items;
loop {
let mut progressed = false;
let mut next_pending: Vec<Item> = Vec::new();
for item in pending {
let name: Option<String> = match &item {
Item::Struct(s) => Some(s.name.clone()),
Item::Union(u) => Some(u.name.clone()),
Item::Enum(e) => Some(e.name.clone()),
_ => None,
};
let ready: bool = dependencies(&item).iter().all(|dep| emitted.contains(dep));
if ready {
if let Some(name) = name {
emitted.insert(name);
}
ordered.push(item);
progressed = true;
} else {
next_pending.push(item);
}
}
if next_pending.is_empty() {
break;
}
if !progressed {
ordered.extend(next_pending);
break;
}
pending = next_pending;
}
ordered
}
fn cpp_dependency_ordered(items: Vec<Item>) -> Vec<Item> {
use std::collections::HashSet;
let aggregate_names: HashSet<String> = items
.iter()
.filter_map(|item| match item {
Item::Struct(s) => Some(s.name.clone()),
Item::Union(u) => Some(u.name.clone()),
_ => None,
})
.collect();
let dependencies = |item: &Item| -> Vec<String> {
let fields: Vec<&str> = match item {
Item::Struct(s) => s.fields.iter().map(|f| f.rust_type.as_str()).collect(),
Item::Union(u) => u.variants.iter().map(|v| v.type_name.as_str()).collect(),
_ => Vec::new(),
};
fields
.iter()
.filter_map(|rust_type| CppGenerator::value_dependency(rust_type))
.filter(|dep| aggregate_names.contains(dep))
.collect()
};
let mut ordered: Vec<Item> = Vec::with_capacity(items.len());
let mut emitted: HashSet<String> = HashSet::new();
let mut pending: Vec<Item> = items;
loop {
let mut progressed = false;
let mut next_pending: Vec<Item> = Vec::new();
for item in pending {
let name = match &item {
Item::Struct(s) => Some(s.name.clone()),
Item::Union(u) => Some(u.name.clone()),
_ => None,
};
let ready = dependencies(&item).iter().all(|dep| emitted.contains(dep));
if ready {
if let Some(name) = name {
emitted.insert(name);
}
ordered.push(item);
progressed = true;
} else {
next_pending.push(item);
}
}
if next_pending.is_empty() {
break;
}
if !progressed {
ordered.extend(next_pending);
break;
}
pending = next_pending;
}
ordered
}
fn python_dependency_ordered(items: Vec<Item>) -> Vec<Item> {
use std::collections::HashSet;
let aggregate_names: HashSet<String> = items
.iter()
.filter_map(|item| match item {
Item::Struct(s) => Some(s.name.clone()),
Item::Enum(e) => Some(e.name.clone()),
Item::Union(u) => Some(u.name.clone()),
_ => None,
})
.collect();
let dependencies = |item: &Item| -> Vec<String> {
let field_types: Vec<&str> = match item {
Item::Struct(s) => s.fields.iter().map(|f| f.rust_type.as_str()).collect(),
Item::Union(u) => u.variants.iter().map(|v| v.type_name.as_str()).collect(),
_ => Vec::new(),
};
field_types
.iter()
.flat_map(|rust_type| PythonGenerator::type_dependencies(rust_type))
.filter(|dep| aggregate_names.contains(dep))
.collect()
};
let mut ordered: Vec<Item> = Vec::with_capacity(items.len());
let mut emitted: HashSet<String> = HashSet::new();
let mut pending: Vec<Item> = items;
loop {
let mut progressed = false;
let mut next_pending: Vec<Item> = Vec::new();
for item in pending {
let name: Option<String> = match &item {
Item::Struct(s) => Some(s.name.clone()),
Item::Enum(e) => Some(e.name.clone()),
Item::Union(u) => Some(u.name.clone()),
_ => None,
};
let ready: bool = dependencies(&item).iter().all(|dep| emitted.contains(dep));
if ready {
if let Some(name) = name {
emitted.insert(name);
}
ordered.push(item);
progressed = true;
} else {
next_pending.push(item);
}
}
if next_pending.is_empty() {
break;
}
if !progressed {
ordered.extend(next_pending);
break;
}
pending = next_pending;
}
ordered
}
impl TargetLang {
fn generated_filenames(&self) -> Vec<&'static str> {
vec![self.output_filename()]
}
}
fn get_inline_helpers(lang: TargetLang) -> Vec<(String, String)> {
match lang {
TargetLang::CSharp => vec![
(
"StringViewHelper.cs".to_string(),
HELPER_CSHARP_STRING_VIEW.to_string(),
),
("Hashing.cs".to_string(), HELPER_CSHARP_HASHING.to_string()),
],
TargetLang::Lua => vec![
("string_view_helper.lua".to_string(), HELPER_LUA.to_string()),
("hashing.lua".to_string(), HELPER_LUA_HASHING.to_string()),
],
TargetLang::JavaScript => {
vec![("string_view_helper.ts".to_string(), HELPER_JS.to_string())]
}
TargetLang::Cpp => vec![("string_view_helper.hpp".to_string(), HELPER_CPP.to_string())],
TargetLang::Python => vec![("hashing.py".to_string(), HELPER_PYTHON_HASHING.to_string())],
}
}
fn delete_old_generated_files(lang: TargetLang, abi_dir: &Path) {
for filename in lang.generated_filenames() {
let path = abi_dir.join(filename);
if path.exists() {
if let Err(e) = fs::remove_file(&path) {
println!(
"cargo:warning=Failed to delete old file {}: {}",
path.display(),
e
);
}
}
}
}
fn strip_auto_generated_header(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut start = 0;
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("// THIS FILE IS AUTO-GENERATED")
|| trimmed.starts_with("-- THIS FILE IS AUTO-GENERATED")
|| trimmed.starts_with("# THIS FILE IS AUTO-GENERATED")
|| trimmed.starts_with("// DO NOT EDIT")
|| trimmed.starts_with("-- DO NOT EDIT")
|| trimmed.starts_with("# DO NOT EDIT")
|| (trimmed.is_empty() && start == i)
{
start = i + 1;
continue;
}
if !trimmed.is_empty()
&& !trimmed.starts_with("///")
&& !trimmed.starts_with("/**")
&& !trimmed.starts_with("// @")
&& !trimmed.starts_with("-- @")
&& !trimmed.starts_with("* @")
&& !trimmed.starts_with(" *")
{
break;
}
}
lines[start..].join("\n")
}
fn extract_lua_helper_methods(content: &str) -> String {
let mut methods = Vec::new();
let mut in_function = false;
let mut depth = 0;
let mut current = String::new();
for line in content.lines() {
if !in_function {
let trimmed = line.trim();
if trimmed.starts_with("function M.") || trimmed.starts_with("function M ") {
in_function = true;
depth = 0;
current.clear();
current.push_str(line);
current.push('\n');
depth += count_lua_openers(trimmed);
}
} else {
current.push_str(line);
current.push('\n');
let trimmed = line.trim();
depth += count_lua_openers(trimmed);
if trimmed == "end" || trimmed.starts_with("end ") || trimmed.starts_with("end--") {
depth = depth.saturating_sub(1);
}
if depth == 0 {
methods.push(current.trim().to_string());
current.clear();
in_function = false;
}
}
}
if in_function && !current.trim().is_empty() {
methods.push(current.trim().to_string());
}
methods.join("\n\n")
}
fn count_lua_openers(line: &str) -> i32 {
let mut count = 0i32;
let trimmed = line.trim();
if trimmed.starts_with("function ") || trimmed.starts_with("function(") {
count += 1;
}
if trimmed.starts_with("if ") || trimmed == "if" {
count += 1;
}
if trimmed.starts_with("for ") || trimmed == "for" {
count += 1;
}
if trimmed.starts_with("while ") || trimmed == "while" {
count += 1;
}
if trimmed.contains(" do") || trimmed.ends_with(" do") {
count += 1;
}
if trimmed.contains(" then") || trimmed.ends_with(" then") {
}
count
}
fn merge_helpers_into_generated(
lang: TargetLang,
generated_code: &str,
helpers: &[(String, String)],
) -> String {
if helpers.is_empty() {
return generated_code.to_string();
}
match lang {
TargetLang::CSharp => merge_csharp_helpers(generated_code, helpers),
TargetLang::Lua => merge_lua_helpers(generated_code, helpers),
TargetLang::JavaScript => merge_js_helpers(generated_code, helpers),
TargetLang::Cpp => merge_cpp_helpers(generated_code, helpers),
TargetLang::Python => merge_python_helpers(generated_code, helpers),
}
}
fn merge_python_helpers(generated_code: &str, helpers: &[(String, String)]) -> String {
let mut result: String = generated_code.to_string();
result.push_str("\n\n# ─── Helper Methods (embedded by the build script) ───\n");
for (_filename, contents) in helpers {
let cleaned: String = strip_auto_generated_header(contents);
let trimmed: &str = cleaned.trim();
if trimmed.is_empty() {
continue;
}
let body: String = trimmed
.lines()
.filter(|line| {
let lt: &str = line.trim();
!lt.starts_with("import ") && !lt.starts_with("from ")
})
.collect::<Vec<&str>>()
.join("\n");
result.push('\n');
result.push_str(&body);
result.push('\n');
}
result
}
fn merge_csharp_helpers(generated_code: &str, helpers: &[(String, String)]) -> String {
let mut merged = generated_code.to_string();
if let Some(pos) = merged.rfind('}') {
let mut helper_block =
String::from("\n// ─── Helper Methods (embedded by the build script) ───\n\n");
for (_filename, contents) in helpers {
let cleaned = strip_auto_generated_header(contents);
let trimmed = cleaned.trim();
if !trimmed.is_empty() {
let body = extract_csharp_class_body(trimmed);
helper_block.push_str(&body);
helper_block.push('\n');
}
}
merged.insert_str(pos, &helper_block);
}
merged
}
fn extract_csharp_class_body(content: &str) -> String {
let mut result = String::new();
let mut in_namespace_brace = false;
let mut brace_depth = 0;
let mut skip_block = false;
let mut using_lines = String::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("using ") && !in_namespace_brace {
if !trimmed.contains("System.Runtime.InteropServices")
&& !trimmed.contains("System.Text")
{
using_lines.push_str(line);
using_lines.push('\n');
}
continue;
}
if trimmed.starts_with("namespace ") {
if trimmed.ends_with('{') {
in_namespace_brace = true;
brace_depth = 1;
}
continue;
}
if in_namespace_brace {
for ch in line.chars() {
match ch {
'{' => brace_depth += 1,
'}' => {
brace_depth -= 1;
if brace_depth == 0 {
in_namespace_brace = false;
skip_block = true;
}
}
_ => {}
}
}
if skip_block {
skip_block = false;
continue;
}
}
if result.is_empty() && trimmed.is_empty() {
continue;
}
result.push_str(line);
result.push('\n');
}
let body = result.trim();
if body.is_empty() {
return String::new();
}
if using_lines.trim().is_empty() {
body.to_string()
} else {
format!("{}\n{}", using_lines.trim(), body)
}
}
fn merge_lua_helpers(generated_code: &str, helpers: &[(String, String)]) -> String {
let mut helper_block = String::new();
helper_block.push_str("\n-- ─── Helper Methods (embedded by the build script) ───\n\n");
for (_filename, contents) in helpers {
let cleaned = strip_auto_generated_header(contents);
let methods = extract_lua_helper_methods(&cleaned);
if !methods.trim().is_empty() {
helper_block.push_str(&methods);
helper_block.push_str("\n\n");
}
}
if let Some(pos) = generated_code.rfind("return M") {
let mut result = generated_code[..pos].to_string();
result.push_str(&helper_block);
result.push_str("return M\n");
result
} else {
let mut result = generated_code.to_string();
result.push_str(&helper_block);
result
}
}
fn merge_js_helpers(generated_code: &str, helpers: &[(String, String)]) -> String {
let mut result = generated_code.to_string();
result.push_str("\n// ─── Helper Methods (embedded by the build script) ───\n\n");
for (_filename, contents) in helpers {
let cleaned = strip_auto_generated_header(contents);
let trimmed = cleaned.trim();
if !trimmed.is_empty() {
let body: String = trimmed
.lines()
.filter(|line| {
let lt = line.trim();
!lt.starts_with("import ")
})
.collect::<Vec<&str>>()
.join("\n");
result.push_str(&body);
result.push_str("\n\n");
}
}
result
}
fn merge_cpp_helpers(generated_code: &str, helpers: &[(String, String)]) -> String {
let mut result = generated_code.to_string();
result.push_str("\n// ─── Helper Methods (embedded by the build script) ───\n");
for (_filename, contents) in helpers {
let cleaned = strip_auto_generated_header(contents);
let trimmed = cleaned.trim();
if trimmed.is_empty() {
continue;
}
let body: String = trimmed
.lines()
.filter(|line| {
let lt = line.trim();
!lt.starts_with("#pragma once")
&& !lt.starts_with("#include \"abi.hpp\"")
&& !lt.starts_with("#include <cstring>")
&& !lt.starts_with("#include <string>")
&& !lt.starts_with("#include <string_view>")
&& !lt.starts_with("#include <vector>")
})
.collect::<Vec<&str>>()
.join("\n");
result.push_str(&body);
result.push('\n');
}
result
}
pub fn generate_all_sdks(
abi_types: &mut AbiTypes,
workspace_root: &Path,
tracked_files: &[PathBuf],
) -> Result<(), Box<dyn std::error::Error>> {
populate_size_hints(abi_types);
validate_representable_types(abi_types)
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
for path in tracked_files {
println!("cargo:rerun-if-changed={}", path.display());
}
let languages: [TargetLang; 5] = [
TargetLang::Cpp,
TargetLang::CSharp,
TargetLang::Python,
TargetLang::Lua,
TargetLang::JavaScript,
];
for lang in languages {
let abi_dir: PathBuf = workspace_root
.join("sdks")
.join(lang.language_name())
.join("abi");
let helpers = get_inline_helpers(lang);
delete_old_generated_files(lang, &abi_dir);
let mut sdk: String = generate_language_sdk(lang, abi_types);
sdk = merge_helpers_into_generated(lang, &sdk, &helpers);
let output_path: PathBuf = if lang.subdir().is_empty() {
abi_dir.join(lang.output_filename())
} else {
abi_dir.join(lang.subdir()).join(lang.output_filename())
};
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&output_path, sdk)?;
}
generate_layout_tests(abi_types, workspace_root)?;
Ok(())
}
fn generate_layout_tests(
abi_types: &AbiTypes,
workspace_root: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let sized_structs: Vec<(&str, usize)> = abi_types
.structs
.iter()
.filter_map(|s| s.size_hint.map(|size| (s.name.as_str(), size)))
.collect();
if sized_structs.is_empty() {
return Ok(());
}
let python_tests = generate_python_layout_tests(&sized_structs);
let python_dir = workspace_root.join("sdks/python/abi");
std::fs::create_dir_all(&python_dir)?;
std::fs::write(python_dir.join("test_layout.py"), python_tests)?;
let csharp_tests = generate_csharp_layout_tests(&sized_structs);
let csharp_dir = workspace_root.join("sdks/csharp/abi.tests");
std::fs::create_dir_all(&csharp_dir)?;
std::fs::write(csharp_dir.join("LayoutTests.cs"), csharp_tests)?;
let lua_tests = generate_lua_layout_tests(&sized_structs);
let lua_dir = workspace_root.join("sdks/lua/abi");
std::fs::create_dir_all(&lua_dir)?;
std::fs::write(lua_dir.join("test_layout.lua"), lua_tests)?;
let js_tests = generate_js_layout_tests(&sized_structs);
let js_dir = workspace_root.join("sdks/js/abi");
std::fs::create_dir_all(&js_dir)?;
std::fs::write(js_dir.join("test_layout.ts"), js_tests)?;
let cpp_tests = generate_cpp_layout_tests(&sized_structs);
let cpp_dir = workspace_root.join("sdks/cpp/abi");
std::fs::create_dir_all(&cpp_dir)?;
std::fs::write(cpp_dir.join("test_layout.cpp"), cpp_tests)?;
Ok(())
}
fn generate_python_layout_tests(sized_structs: &[(&str, usize)]) -> String {
let mut output = String::new();
output.push_str("# Layout tests for polyplug ABI structs.\n");
output.push_str("# AUTO-GENERATED by polyplug_abi build script — do not edit.\n\n");
output.push_str("import ctypes\n\n");
output.push_str("from abi import (\n");
for (name, _) in sized_structs {
output.push_str(&format!(" {},\n", name));
}
output.push_str(")\n\n\n");
for (name, size) in sized_structs {
let test_name = to_snake_case(name);
output.push_str(&format!(
"def test_{}_size():\n assert ctypes.sizeof({}) == {}, \
f\"{} expected {} bytes, got {{ctypes.sizeof({})}}\"\n\n\n",
test_name, name, size, name, size, name
));
}
output
}
fn generate_csharp_layout_tests(sized_structs: &[(&str, usize)]) -> String {
let mut output = String::new();
output.push_str("// Layout tests for polyplug ABI structs.\n");
output.push_str("// AUTO-GENERATED by polyplug_abi build script — do not edit.\n\n");
output.push_str("using System.Runtime.InteropServices;\n");
output.push_str("using Xunit;\n\n");
output.push_str("namespace Polyplug.Abi.Tests\n{\n");
output.push_str(" public class LayoutTests\n {\n");
for (name, size) in sized_structs {
let test_name = format!("{}Is{}Bytes", name, size);
output.push_str(&format!(
" [Fact]\n public void {}() => \
Assert.Equal({}, Marshal.SizeOf<{}>());\n\n",
test_name, size, name
));
}
let offset_asserts: [(&str, &str, usize); 8] = [
("HostApi", "UnloadBundle", 136),
("HostApi", "Log", 144),
("HostApi", "CreateGuestInstance", 152),
("HostApi", "DestroyGuestInstance", 160),
("HostApi", "Reserved", 176),
("RuntimeConfig", "Log", 24),
("RuntimeConfig", "LogUserData", 32),
("RuntimeConfig", "LogMaxLevel", 40),
];
for (struct_name, field, offset) in offset_asserts {
output.push_str(&format!(
" [Fact]\n public void {struct_name}{field}AtOffset{offset}() => \
Assert.Equal((nint){offset}, Marshal.OffsetOf<{struct_name}>(nameof({struct_name}.{field})));\n\n",
));
}
output.push_str(" }\n}\n");
output
}
fn generate_lua_layout_tests(sized_structs: &[(&str, usize)]) -> String {
let mut output = String::new();
output.push_str("-- Layout tests for polyplug ABI structs.\n");
output.push_str("-- AUTO-GENERATED by polyplug_abi build script — do not edit.\n\n");
output.push_str(
"local script_dir = (arg and arg[0] or \"\"):match(\"^(.*[/\\\\])\") or \"./\"\n",
);
output.push_str("package.path = script_dir .. \"?.lua;\" .. package.path\n");
output.push_str("local ffi = require(\"ffi\")\n");
output.push_str("require(\"abi\")\n\n");
for (name, size) in sized_structs {
output.push_str(&format!(
"assert(ffi.sizeof(\"{}\") == {}, \"{} size mismatch\")\n",
name, size, name
));
}
output.push_str("\nprint(\"All layout tests passed!\")\n");
output
}
fn generate_js_layout_tests(sized_structs: &[(&str, usize)]) -> String {
let mut output = String::new();
output.push_str("// Layout tests for polyplug ABI structs.\n");
output.push_str("// AUTO-GENERATED by polyplug_abi build script — do not edit.\n\n");
output.push_str("import {\n");
for (name, _) in sized_structs {
output.push_str(&format!(
" {}_SIZE,\n",
to_upper_snake_case_for_generate(name)
));
}
output.push_str("} from \"./abi.ts\";\n");
output.push_str("import { assert } from \"jsr:@std/assert\";\n\n");
for (name, size) in sized_structs {
let const_name = format!("{}_SIZE", to_upper_snake_case_for_generate(name));
output.push_str(&format!(
"Deno.test(\"{} is {} bytes\", () => {{\n assert({} === {});\n}});\n\n",
name, size, const_name, size
));
}
output
}
fn generate_cpp_layout_tests(sized_structs: &[(&str, usize)]) -> String {
let mut output = String::new();
output.push_str("// Layout tests for polyplug ABI structs.\n");
output.push_str("// AUTO-GENERATED by polyplug_abi build script — do not edit.\n\n");
output.push_str("#include \"polyplug/abi.hpp\"\n\n");
for (name, size) in sized_structs {
output.push_str(&format!(
"static_assert(sizeof({}) == {}, \"{} size mismatch\");\n",
name, size, name
));
}
output
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
fn to_upper_snake_case_for_generate(s: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = s.chars().collect();
for (i, c) in chars.iter().enumerate() {
if c.is_uppercase() {
if i > 0 {
let prev = chars[i - 1];
if prev.is_ascii_lowercase()
|| (prev.is_uppercase()
&& i + 1 < chars.len()
&& chars[i + 1].is_ascii_lowercase())
{
result.push('_');
}
}
result.push(*c);
} else {
result.push(c.to_ascii_uppercase());
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::build::types::{AbiConst, AbiStruct};
#[test]
fn test_target_lang_language_name() {
assert_eq!(TargetLang::Cpp.language_name(), "cpp");
assert_eq!(TargetLang::CSharp.language_name(), "csharp");
assert_eq!(TargetLang::Python.language_name(), "python");
assert_eq!(TargetLang::Lua.language_name(), "lua");
assert_eq!(TargetLang::JavaScript.language_name(), "js");
}
#[test]
fn test_generate_language_sdk_cpp() {
let mut abi_types: AbiTypes = AbiTypes::new();
abi_types.add_const(AbiConst {
name: String::from("TEST_CONST"),
rust_type: String::from("u32"),
value: String::from("42"),
doc: Some(String::from("Test constant.")),
});
let sdk: String = generate_language_sdk(TargetLang::Cpp, &abi_types);
assert!(sdk.contains("#pragma once"));
assert!(sdk.contains("#include <cstdint>"));
assert!(sdk.contains("TEST_CONST"));
}
#[test]
fn test_generate_language_sdk_python() {
let mut abi_types: AbiTypes = AbiTypes::new();
abi_types.add_const(AbiConst {
name: String::from("TEST_CONST"),
rust_type: String::from("u32"),
value: String::from("42"),
doc: Some(String::from("Test constant.")),
});
let sdk: String = generate_language_sdk(TargetLang::Python, &abi_types);
assert!(sdk.contains("import ctypes"));
assert!(sdk.contains("TEST_CONST"));
}
#[test]
fn test_populate_size_hints() {
use crate::build::types::AbiField;
let mut abi_types: AbiTypes = AbiTypes::new();
abi_types.add_struct(AbiStruct {
name: String::from("RuntimeConfig"),
fields: vec![],
doc: None,
repr_c: true,
size_hint: None,
});
abi_types.add_struct(AbiStruct {
name: String::from("GuestContractHandle"),
fields: vec![],
doc: None,
repr_c: true,
size_hint: None,
});
abi_types.add_struct(AbiStruct {
name: String::from("UnknownStruct"),
fields: vec![],
doc: None,
repr_c: true,
size_hint: None,
});
populate_size_hints(&mut abi_types);
assert_eq!(
abi_types.structs[0].size_hint,
Some(16),
"RuntimeConfig should be 16 bytes"
);
assert_eq!(
abi_types.structs[1].size_hint,
Some(8),
"GuestContractHandle should be 8 bytes"
);
assert_eq!(
abi_types.structs[2].size_hint, None,
"Unknown struct should have no size hint"
);
}
#[test]
fn test_cpp_output_contains_static_assert() {
use crate::build::types::AbiField;
let mut abi_types: AbiTypes = AbiTypes::new();
abi_types.add_struct(AbiStruct {
name: String::from("RuntimeConfig"),
fields: vec![AbiField {
name: String::from("compatibility"),
rust_type: String::from("u32"),
doc: None,
}],
doc: None,
repr_c: true,
size_hint: Some(16),
});
let sdk: String = generate_language_sdk(TargetLang::Cpp, &abi_types);
assert!(
sdk.contains("static_assert(sizeof(RuntimeConfig) == 16"),
"C++ should contain static_assert for RuntimeConfig: {}",
sdk
);
}
#[test]
fn test_python_output_contains_sizeof_assertions() {
use crate::build::types::AbiField;
let mut abi_types: AbiTypes = AbiTypes::new();
abi_types.add_struct(AbiStruct {
name: String::from("RuntimeConfig"),
fields: vec![AbiField {
name: String::from("compatibility"),
rust_type: String::from("u32"),
doc: None,
}],
doc: None,
repr_c: true,
size_hint: Some(16),
});
let sdk: String = generate_language_sdk(TargetLang::Python, &abi_types);
assert!(
sdk.contains("assert ctypes.sizeof(RuntimeConfig) == 16"),
"Python should contain ctypes.sizeof assertion for RuntimeConfig: {}",
sdk
);
}
}