Files
sbox-public/engine/Tools/InteropGen/Writer/ManagedWriter.cs
2026-06-11 10:36:53 +00:00

245 lines
8.1 KiB
C#

using System.Collections.Generic;
using System.Linq;
namespace Facepunch.InteropGen;
/// <summary>
/// Emits the managed (C#) side: the typed wrappers (imports), the [UnmanagedCallersOnly] thunks
/// (exports), and the NativeInterop bootstrap that wires the two sides' function pointers together.
/// </summary>
internal partial class ManagedWriter : BaseWriter
{
public ManagedWriter( Definition definitions, string targetName ) : base( definitions, targetName )
{
}
public override void Generate()
{
Header();
Imports();
PointerStructs();
StartBlock( $"namespace {definitions.ManagedNamespace}" );
{
Exports();
NativeInterop();
}
EndBlock();
}
private void Header()
{
WriteLine( "// <auto-generated>" );
WriteLine( "// This file was generated by a tool." );
WriteLine( "// Changes to this file may cause incorrect behavior and will be lost if" );
WriteLine( "// the code is regenerated." );
WriteLine( "// </auto-generated>" );
WriteLine( "" );
WriteLine( "using System;" );
WriteLine( "using System.Collections.Generic;" );
WriteLine( "using System.Runtime.CompilerServices;" );
WriteLine( "using System.Runtime.InteropServices;" );
WriteLine( "" );
}
/// <summary>
/// The NativeInterop bootstrap class: loads the native dll, calls its igen_* initializer to swap
/// function pointer tables, and distributes the received native pointers into each wrapper's table.
/// </summary>
private void NativeInterop()
{
IEnumerable<Function> imports = definitions.ManagedClasses.Where( x => !Skip.ShouldSkip( x ) ).SelectMany( x => x.Functions );
StartBlock( "internal unsafe static partial class NativeInterop" );
{
WriteLine( "static IntPtr _nativeLibraryHandle;" );
WriteLine( "static bool _initialized;" );
WriteLine();
ErrorFunction();
WriteLine( "[UnmanagedFunctionPointer( CallingConvention.Cdecl )]" );
WriteLine( "delegate void NetCoreImportDelegate( int hash, void* imports, void* exports, int* structSizes );" );
WriteLine();
StartBlock( "internal static void Initialize()" );
{
WriteLine( "if ( _initialized ) return;" );
WriteLine();
LoadNativeLibrary();
ManagedFunctionTable( imports );
{
WriteLine();
WriteLine( $"var nativeFunctions = new IntPtr[{NativeExportSlots().Count()}];" );
}
StructSizeTable();
WriteLine();
WriteLine();
WriteLine( "fixed ( void* m = managedFunctions )" );
WriteLine( "fixed ( void* n = nativeFunctions )" );
WriteLine( "fixed ( int* s = structSizes )" );
StartBlock( null );
WriteLine( $"nativeInit( {definitions.Hash}, m, n, s );" );
EndBlock();
WriteLine();
BindNativeFunctions();
WriteLine( "_initialized = true;" );
}
EndBlock();
StartBlock( "internal static void Free()" );
{
WriteLine( "if ( _nativeLibraryHandle == IntPtr.Zero ) return;" );
WriteLine( "NativeLibrary.Free( _nativeLibraryHandle );" );
WriteLine( "_nativeLibraryHandle = IntPtr.Zero;" );
WriteLine( "_initialized = false;" );
}
EndBlock();
}
EndBlock();
}
/// <summary>
/// Load the native dll and resolve its igen_* initializer into a callable delegate.
/// </summary>
private void LoadNativeLibrary()
{
WriteLine( $"var _nativeDllName = Sandbox.Interop.GetNativeLibraryName( \"{definitions.NativeDll}\" );" );
WriteLine( $"if ( !NativeLibrary.TryLoad( System.IO.Path.Combine( NetCore.NativeDllPath, _nativeDllName ), out var nativeDll ) )" );
WriteLine( $" Sandbox.Interop.NativeAssemblyLoadFailed( _nativeDllName );" );
WriteLine( "_nativeLibraryHandle = nativeDll;" );
WriteLine();
WriteLine( $"IntPtr nativeInitPtr = NativeLibrary.GetExport( nativeDll, \"igen_{definitions.Ident}\" );" );
WriteLine( $"if ( nativeInitPtr == IntPtr.Zero ) throw new System.Exception( $\"Couldn't find igen_{definitions.Ident} in {{_nativeDllName}}\" );" );
WriteLine();
WriteLine( $"var nativeInit = Marshal.GetDelegateForFunctionPointer<NetCoreImportDelegate>( nativeInitPtr );" );
WriteLine( $"if ( nativeInit == null ) throw new System.Exception( $\"Couldn't load from {{_nativeDllName}}\" );" );
}
/// <summary>
/// The managedFunctions array: a pointer to the [UnmanagedCallersOnly] thunk for every function
/// managed exports to native, in the order native reads them back.
/// </summary>
private void ManagedFunctionTable( IEnumerable<Function> imports )
{
WriteLine();
WriteLine( $"var managedFunctions = new IntPtr[{imports.Count()}]" );
StartBlock( null );
foreach ( Function f in imports )
{
Class c = f.Class;
IEnumerable<string> managedArgs = c.SelfArg( false, f.Static ).Concat( f.Parameters ).Concat( new[] { f.Return } ).Where( x => x.IsRealArgument ).Select( x => $"{x.DelegateType( Side.Managed, Dir.Incoming )}" );
string managedArgss = $"{string.Join( ", ", managedArgs )}";
WriteLine( $"(IntPtr) (delegate* unmanaged<{managedArgss}>) &Exports.{f.MangledName}," );
}
EndBlock( ";" );
}
/// <summary>
/// The structSizes array, so native can verify both sides agree on every struct's size.
/// </summary>
private void StructSizeTable()
{
WriteLine();
WriteLine( $"var structSizes = new int[]" );
StartBlock( null );
foreach ( Struct s in definitions.Structs )
{
if ( Skip.ShouldSkip( s ) )
{
continue;
}
string size_of = $"sizeof( {s.ManagedNameWithNamespace} )";
WriteLine( $"{size_of}," );
}
EndBlock( ";" );
}
/// <summary>
/// Read each received native pointer out of the nativeFunctions array into the matching wrapper's
/// function pointer field. Slot order comes from <see cref="NativeExportTable"/> on both sides.
/// </summary>
private void BindNativeFunctions()
{
WriteLine( $"var onError = Marshal.GetDelegateForFunctionPointer<_ErrorFunction>( nativeFunctions[0] );" );
WriteLine();
StartBlock( "try" );
int index = 0;
foreach ( NativeSlot slot in NativeExportSlots() )
{
// Slot 0 (the error function) was already read into onError above.
if ( slot.Kind == NativeSlotKind.Error )
{
index++;
continue;
}
string namespc = $"{slot.Class.ManagedNamespace}.{slot.Class.ManagedName}".Trim( '.' );
switch ( slot.Kind )
{
case NativeSlotKind.CastFromTo:
WriteLine( $"{namespc}.{InternalNative}.From_{slot.BaseClass.ManagedName}_To_{slot.Class.ManagedName} = (delegate* unmanaged[SuppressGCTransition]< IntPtr, IntPtr >) nativeFunctions[{index}];" );
break;
case NativeSlotKind.CastToFrom:
WriteLine( $"{namespc}.{InternalNative}.To_{slot.BaseClass.ManagedName}_From_{slot.Class.ManagedName} = (delegate* unmanaged[SuppressGCTransition]< IntPtr, IntPtr >) nativeFunctions[{index}];" );
break;
case NativeSlotKind.Function:
{
Function f = slot.Function;
IEnumerable<string> managedArgs = slot.Class.SelfArg( false, f.Static ).Concat( f.Parameters ).Where( x => x.IsRealArgument ).Select( x => $"{x.DelegateType( Side.Managed, Dir.Outgoing )}" ).Concat( new[] { f.Return.DelegateType( Side.Managed, Dir.Incoming ) } );
string managedArgss = $"{string.Join( ", ", managedArgs )}";
string nogc = f.IsNoGC ? "[SuppressGCTransition]" : "";
WriteLine( $"{namespc}.{InternalNative}.{f.MangledName} = (delegate* unmanaged{nogc}< {managedArgss} >) nativeFunctions[{index}];" );
break;
}
case NativeSlotKind.VariableGet:
WriteLine( $"{namespc}.{InternalNative}.Get__{slot.Variable.MangledName} = (delegate* unmanaged[SuppressGCTransition]<IntPtr, {slot.Variable.Return.DelegateType( Side.Managed, Dir.Incoming )}>)( nativeFunctions[{index}] );" );
break;
case NativeSlotKind.VariableSet:
WriteLine( $"{namespc}.{InternalNative}.Set__{slot.Variable.MangledName} = (delegate* unmanaged[SuppressGCTransition]<IntPtr, {slot.Variable.Return.DelegateType( Side.Managed, Dir.Outgoing )}, void>)( nativeFunctions[{index}] );" );
break;
}
index++;
}
EndBlock();
StartBlock( "catch ( System.Exception ___e )" );
{
WriteLine( "onError( $\"{___e.Message}\\n\\n{___e.StackTrace}\" );" );
}
EndBlock();
}
private void ErrorFunction()
{
WriteLine( "[UnmanagedFunctionPointer( CallingConvention.Cdecl )]" );
WriteLine( "internal delegate void _ErrorFunction( string message );" );
WriteLine();
}
}