{%- if module.needs_ffi_buf() %}
[StructLayout(LayoutKind.Sequential)]
internal struct FfiBuf
{
public IntPtr ptr;
public UIntPtr len;
public UIntPtr cap;
public UIntPtr align;
}
{% endif %}
{%- if module.needs_wire_reader() %}
internal sealed class WireReader
{
// Reads directly from the unmanaged FfiBuf pointer — no eager copy into
// a managed byte[]. Safe APIs only: Marshal.Read* for primitives,
// Marshal.PtrToStringUTF8 for length-prefixed UTF-8, Marshal.Copy for
// bytes. BitConverter.IsLittleEndian is a JIT intrinsic that folds the
// byte-swap branch away on little-endian hosts (all our supported
// targets).
private readonly IntPtr _ptr;
private readonly int _length;
private int _pos;
internal WireReader(FfiBuf buf)
{
_ptr = buf.ptr;
_length = buf.ptr == IntPtr.Zero ? 0 : checked((int)(nuint)buf.len);
_pos = 0;
}
internal bool ReadBool() => ReadU8() != 0;
internal sbyte ReadI8()
{
Require(1, "i8");
sbyte v = (sbyte)Marshal.ReadByte(_ptr, _pos);
_pos += 1;
return v;
}
internal byte ReadU8()
{
Require(1, "u8");
byte v = Marshal.ReadByte(_ptr, _pos);
_pos += 1;
return v;
}
internal short ReadI16()
{
Require(2, "i16");
short v = Marshal.ReadInt16(_ptr, _pos);
_pos += 2;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal ushort ReadU16()
{
Require(2, "u16");
ushort v = (ushort)Marshal.ReadInt16(_ptr, _pos);
_pos += 2;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal int ReadI32()
{
Require(4, "i32");
int v = Marshal.ReadInt32(_ptr, _pos);
_pos += 4;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal uint ReadU32()
{
Require(4, "u32");
uint v = (uint)Marshal.ReadInt32(_ptr, _pos);
_pos += 4;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal long ReadI64()
{
Require(8, "i64");
long v = Marshal.ReadInt64(_ptr, _pos);
_pos += 8;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal ulong ReadU64()
{
Require(8, "u64");
ulong v = (ulong)Marshal.ReadInt64(_ptr, _pos);
_pos += 8;
return BitConverter.IsLittleEndian ? v : BinaryPrimitives.ReverseEndianness(v);
}
internal float ReadF32()
{
Require(4, "f32");
int bits = Marshal.ReadInt32(_ptr, _pos);
_pos += 4;
return BitConverter.Int32BitsToSingle(BitConverter.IsLittleEndian ? bits : BinaryPrimitives.ReverseEndianness(bits));
}
internal double ReadF64()
{
Require(8, "f64");
long bits = Marshal.ReadInt64(_ptr, _pos);
_pos += 8;
return BitConverter.Int64BitsToDouble(BitConverter.IsLittleEndian ? bits : BinaryPrimitives.ReverseEndianness(bits));
}
// usize/isize travel as 64-bit integers on the wire so the wire
// layout stays stable across pointer widths.
internal nint ReadNInt() => (nint)ReadI64();
internal nuint ReadNUInt() => (nuint)ReadU64();
internal string ReadString()
{
int len = ReadI32();
if (len == 0) return "";
if (len < 0) throw new InvalidOperationException("corrupt wire: negative string length");
Require(len, "string payload");
string v = Marshal.PtrToStringUTF8(_ptr + _pos, len)
?? throw new InvalidOperationException("PtrToStringUTF8 returned null");
_pos += len;
return v;
}
internal byte[] ReadBytes()
{
int len = ReadI32();
if (len == 0) return Array.Empty<byte>();
if (len < 0) throw new InvalidOperationException("corrupt wire: negative bytes length");
Require(len, "bytes payload");
byte[] v = new byte[len];
Marshal.Copy(_ptr + _pos, v, 0, len);
_pos += len;
return v;
}
/// Reads the remaining bytes of this buffer as a `T[]`, assuming
/// the buffer holds a raw element array with no length prefix.
/// This matches the wire shape of a top-level `Vec<T>` return,
/// where the FfiBuf's own `len` provides the element count.
internal T[] ReadBlittableArray<T>() where T : unmanaged
{
int byteCount = _length - _pos;
if (byteCount < 0)
throw new InvalidOperationException("corrupt wire: read position past end");
int elementSize = Unsafe.SizeOf<T>();
if (byteCount % elementSize != 0)
throw new InvalidOperationException(
"corrupt wire: blittable array byte count is not a multiple of element size");
if (byteCount == 0) return Array.Empty<T>();
int count = byteCount / elementSize;
T[] result = new T[count];
// Fast path via Marshal.Copy's per-type overloads for the types
// that ship with one (byte, short, int, long, float, double).
// Unsigned primitives and any other unmanaged T fall through to
// a byte scratch buffer reinterpreted via MemoryMarshal.AsBytes.
// Little-endian hosts only; big-endian would need byte-swap.
switch (result)
{
case byte[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, byteCount);
break;
case short[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case int[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case long[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case float[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case double[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
default:
byte[] scratch = new byte[byteCount];
Marshal.Copy(_ptr + _pos, scratch, 0, byteCount);
scratch.AsSpan().CopyTo(MemoryMarshal.AsBytes(result.AsSpan()));
break;
}
_pos += byteCount;
return result;
}
internal bool[] ReadBoolArray()
{
int count = _length - _pos;
if (count < 0)
throw new InvalidOperationException("corrupt wire: read position past end");
if (count == 0) return Array.Empty<bool>();
bool[] result = new bool[count];
for (int i = 0; i < count; i++)
{
result[i] = Marshal.ReadByte(_ptr, _pos + i) != 0;
}
_pos += count;
return result;
}
internal nint[] ReadNIntArray()
{
long[] raw = ReadBlittableArray<long>();
nint[] result = new nint[raw.Length];
for (int i = 0; i < raw.Length; i++) result[i] = (nint)raw[i];
return result;
}
internal nuint[] ReadNUIntArray()
{
long[] raw = ReadBlittableArray<long>();
nuint[] result = new nuint[raw.Length];
for (int i = 0; i < raw.Length; i++) result[i] = (nuint)(ulong)raw[i];
return result;
}
/// Reads a length-prefixed `T[]` whose bytes follow the 4-byte count.
/// Used for `Vec<T>` nested inside an encoded outer container, where
/// the outer codec needs an explicit count to know how far to advance
/// the cursor between sibling elements.
internal T[] ReadLengthPrefixedBlittableArray<T>() where T : unmanaged
{
int count = ReadI32();
if (count < 0) throw new InvalidOperationException("corrupt wire: negative array length");
if (count == 0) return Array.Empty<T>();
int elementSize = Unsafe.SizeOf<T>();
int byteCount = checked(count * elementSize);
Require(byteCount, "blittable array payload");
T[] result = new T[count];
switch (result)
{
case byte[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, byteCount);
break;
case short[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case int[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case long[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case float[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
case double[] dst:
Marshal.Copy(_ptr + _pos, dst, 0, count);
break;
default:
byte[] scratch = new byte[byteCount];
Marshal.Copy(_ptr + _pos, scratch, 0, byteCount);
scratch.AsSpan().CopyTo(MemoryMarshal.AsBytes(result.AsSpan()));
break;
}
_pos += byteCount;
return result;
}
internal bool[] ReadLengthPrefixedBoolArray()
{
int count = ReadI32();
if (count < 0) throw new InvalidOperationException("corrupt wire: negative array length");
if (count == 0) return Array.Empty<bool>();
Require(count, "bool array payload");
bool[] result = new bool[count];
for (int i = 0; i < count; i++)
{
result[i] = Marshal.ReadByte(_ptr, _pos + i) != 0;
}
_pos += count;
return result;
}
internal nint[] ReadLengthPrefixedNIntArray()
{
long[] raw = ReadLengthPrefixedBlittableArray<long>();
nint[] result = new nint[raw.Length];
for (int i = 0; i < raw.Length; i++) result[i] = (nint)raw[i];
return result;
}
internal nuint[] ReadLengthPrefixedNUIntArray()
{
long[] raw = ReadLengthPrefixedBlittableArray<long>();
nuint[] result = new nuint[raw.Length];
for (int i = 0; i < raw.Length; i++) result[i] = (nuint)(ulong)raw[i];
return result;
}
/// Reads a length-prefixed array whose elements are decoded by
/// invoking `read` once per slot. Used for `Vec<T>` where each
/// element has variable wire width (strings, nested encoded vecs,
/// records).
internal T[] ReadEncodedArray<T>(Func<WireReader, T> read)
{
int count = ReadI32();
if (count < 0) throw new InvalidOperationException("corrupt wire: negative array length");
if (count == 0) return Array.Empty<T>();
T[] result = new T[count];
for (int i = 0; i < count; i++)
{
result[i] = read(this);
}
return result;
}
private void Require(int n, string kind)
{
if (n < 0 || n > _length - _pos) throw new InvalidOperationException("corrupt wire: truncated " + kind);
}
}
{% endif %}
{%- if module.needs_wire_writer() %}
internal sealed class WireWriter : IDisposable
{
private const int MinCapacity = 16;
private byte[] _buffer;
private int _pos;
private bool _disposed;
internal WireWriter(int initialCapacity)
{
int cap = Math.Max(initialCapacity, MinCapacity);
_buffer = ArrayPool<byte>.Shared.Rent(cap);
_pos = 0;
_disposed = false;
}
/// <summary>Copy the written bytes into a fresh managed array.</summary>
internal byte[] ToArray()
{
if (_pos == 0) return Array.Empty<byte>();
byte[] result = new byte[_pos];
Buffer.BlockCopy(_buffer, 0, result, 0, _pos);
return result;
}
internal int Position => _pos;
internal void WriteBool(bool v) { EnsureCapacity(1); _buffer[_pos++] = (byte)(v ? 1 : 0); }
internal void WriteI8(sbyte v) { EnsureCapacity(1); _buffer[_pos++] = (byte)v; }
internal void WriteU8(byte v) { EnsureCapacity(1); _buffer[_pos++] = v; }
internal void WriteI16(short v) { EnsureCapacity(2); BinaryPrimitives.WriteInt16LittleEndian(_buffer.AsSpan(_pos), v); _pos += 2; }
internal void WriteU16(ushort v) { EnsureCapacity(2); BinaryPrimitives.WriteUInt16LittleEndian(_buffer.AsSpan(_pos), v); _pos += 2; }
internal void WriteI32(int v) { EnsureCapacity(4); BinaryPrimitives.WriteInt32LittleEndian(_buffer.AsSpan(_pos), v); _pos += 4; }
internal void WriteU32(uint v) { EnsureCapacity(4); BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_pos), v); _pos += 4; }
internal void WriteI64(long v) { EnsureCapacity(8); BinaryPrimitives.WriteInt64LittleEndian(_buffer.AsSpan(_pos), v); _pos += 8; }
internal void WriteU64(ulong v) { EnsureCapacity(8); BinaryPrimitives.WriteUInt64LittleEndian(_buffer.AsSpan(_pos), v); _pos += 8; }
internal void WriteF32(float v) { EnsureCapacity(4); BinaryPrimitives.WriteSingleLittleEndian(_buffer.AsSpan(_pos), v); _pos += 4; }
internal void WriteF64(double v) { EnsureCapacity(8); BinaryPrimitives.WriteDoubleLittleEndian(_buffer.AsSpan(_pos), v); _pos += 8; }
internal void WriteNInt(nint v) => WriteI64((long)v);
internal void WriteNUInt(nuint v) => WriteU64((ulong)v);
internal void WriteString(string v)
{
int byteCount = Encoding.UTF8.GetByteCount(v);
WriteI32(byteCount);
if (byteCount == 0) return;
EnsureCapacity(byteCount);
Encoding.UTF8.GetBytes(v, 0, v.Length, _buffer, _pos);
_pos += byteCount;
}
internal void WriteBytes(byte[] v)
{
WriteI32(v.Length);
if (v.Length == 0) return;
EnsureCapacity(v.Length);
Buffer.BlockCopy(v, 0, _buffer, _pos, v.Length);
_pos += v.Length;
}
internal void WriteBlittableArray<T>(T[] v) where T : unmanaged
{
WriteI32(v.Length);
if (v.Length == 0) return;
int byteCount = checked(v.Length * Unsafe.SizeOf<T>());
EnsureCapacity(byteCount);
// Reinterpret the source T[] as bytes and block-copy into the
// managed buffer. Zero extra allocations, one copy. Little-endian
// hosts only; big-endian would need byte-swap.
MemoryMarshal.AsBytes(v.AsSpan()).CopyTo(_buffer.AsSpan(_pos, byteCount));
_pos += byteCount;
}
internal void WriteBoolArray(bool[] v)
{
WriteI32(v.Length);
if (v.Length == 0) return;
EnsureCapacity(v.Length);
for (int i = 0; i < v.Length; i++)
{
_buffer[_pos + i] = (byte)(v[i] ? 1 : 0);
}
_pos += v.Length;
}
internal void WriteNIntArray(nint[] v)
{
long[] widened = new long[v.Length];
for (int i = 0; i < v.Length; i++) widened[i] = (long)v[i];
WriteBlittableArray(widened);
}
internal void WriteNUIntArray(nuint[] v)
{
long[] widened = new long[v.Length];
for (int i = 0; i < v.Length; i++) widened[i] = (long)(ulong)v[i];
WriteBlittableArray(widened);
}
/// Byte size of an encoded `T[]`: the `sizeof(int)` length prefix
/// plus the per-element size. Size expressions for `Vec<T>` with
/// variable-width elements (strings, nested vecs, records) use this
/// helper to drive the `WireWriter` pre-size so its initial
/// capacity matches the payload and no growth copy is needed.
internal static int EncodedArraySize<T>(T[] v, Func<T, int> sizer)
{
int total = sizeof(int);
for (int i = 0; i < v.Length; i++) total += sizer(v[i]);
return total;
}
private void EnsureCapacity(int additional)
{
if (_pos + additional <= _buffer.Length) return;
int next = Math.Max(_buffer.Length * 2, _pos + additional);
byte[] grown = ArrayPool<byte>.Shared.Rent(next);
Buffer.BlockCopy(_buffer, 0, grown, 0, _pos);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = grown;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = Array.Empty<byte>();
}
}
{% endif %}
internal static class NativeMethods
{
private const string LibName = "{{ module.lib_name }}";
{%- if module.needs_ffi_buf() %}
[DllImport(LibName, EntryPoint = "{{ module.prefix }}_free_buf")]
internal static extern void FreeBuf(FfiBuf buf);
{%- endif %}
{%- for func in module.functions %}
[DllImport(LibName, EntryPoint = "{{ func.ffi_name }}")]
{%- if func.return_type.is_bool() %}
[return: MarshalAs(UnmanagedType.I1)]
{%- endif %}
internal static extern {{ func.native_return_type() }} {{ func.name }}({{ func.native_param_list() }});
{%- endfor %}
{%- for enumeration in module.enums %}
{%- for method in enumeration.methods %}
[DllImport(LibName, EntryPoint = "{{ method.ffi_name }}")]
{%- if method.return_type.is_bool() %}
[return: MarshalAs(UnmanagedType.I1)]
{%- endif %}
internal static extern {{ method.native_return_type() }} {{ method.native_method_name }}({{ method.native_param_list(enumeration.class_name, false) }});
{%- endfor %}
{%- endfor %}
}
}