/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ using Microsoft.Extensions.Logging; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; using System.Threading.Channels; namespace GitHub.Copilot; /// /// Hosts the Copilot runtime in-process by loading the Rust cdylib (runtime.node) /// and speaking JSON-RPC over its C ABI (FFI) instead of spawning a CLI child process /// and communicating over stdio/TCP. /// /// /// The Rust host_start export constructs the server synchronously in this /// process. JSON-RPC frames are pumped across the ABI: writes go to /// connection_write; inbound frames arrive on a native callback that feeds /// . /// /// The native interop layer has two implementations selected by target framework. On /// modern .NET it uses source-generated LibraryImport P/Invoke with an /// UnmanagedCallersOnly function-pointer callback, which is trim- and /// NativeAOT-compatible. On netstandard2.0 (which has neither LibraryImport /// nor NativeLibrary) it falls back to classic delegate-based P/Invoke over a /// hand-rolled dlopen/LoadLibrary loader. Because the library lives at a /// runtime-resolved absolute path, the modern path maps the logical /// via a resolver and the legacy path loads the absolute path /// directly. /// /// internal sealed partial class FfiRuntimeHost : IDisposable { /// Logical name the native interop layer binds the cdylib to. private const string LibraryName = "copilot_runtime"; private readonly ILogger _logger; private readonly string? _cliEntrypoint; private readonly string _libraryPath; private readonly IReadOnlyDictionary? _environment; private readonly IReadOnlyList _args; private readonly CallbackReceiveStream _receiveStream = new(); private CallbackSendStream? _sendStream; private uint _serverId; private uint _connectionId; private bool _disposed; private FfiRuntimeHost(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { _libraryPath = libraryPath; _cliEntrypoint = cliEntrypoint; _environment = environment; _args = args; _logger = logger; } /// The stream JSON-RPC reads server→client frames from. public Stream ReceiveStream => _receiveStream; /// The stream JSON-RPC writes client→server frames to. public Stream SendStream => _sendStream ?? throw new InvalidOperationException("FfiRuntimeHost has not been started."); /// /// Loads the runtime cdylib and prepares the FFI host. /// public static FfiRuntimeHost Create(string libraryPath, string? cliEntrypoint, IReadOnlyDictionary? environment, IReadOnlyList args, ILogger logger) { var fullLibraryPath = Path.GetFullPath(libraryPath); if (!File.Exists(fullLibraryPath)) { throw new InvalidOperationException($"FFI runtime library not found at '{fullLibraryPath}'."); } PrepareNativeLibrary(fullLibraryPath); return new FfiRuntimeHost( fullLibraryPath, cliEntrypoint is null ? null : Path.GetFullPath(cliEntrypoint), environment, args, logger); } /// /// The natural platform shared-library file name for the runtime cdylib, as /// emitted by the .NET build (the .node file renamed to what the Rust cdylib /// would be called on this OS). /// internal static string GetRuntimeLibraryFileName() { if (OperatingSystem.IsWindows()) return "copilot_runtime.dll"; if (OperatingSystem.IsMacOS()) return "libcopilot_runtime.dylib"; return "libcopilot_runtime.so"; } /// /// Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. /// public async Task StartAsync(CancellationToken cancellationToken) { // Keep synchronous native startup off the caller's async context. await Task.Run(() => { var argvJson = BuildArgvJson(_cliEntrypoint, _args); var envJson = BuildEnvJson(_environment); _serverId = NativeHostStart(argvJson, envJson); if (_serverId == 0) { throw new InvalidOperationException( $"copilot_runtime_host_start failed (library '{_libraryPath}')."); } _connectionId = NativeOpenConnection(_serverId); if (_connectionId == 0) { DisposeNativeCallback(); NativeHostShutdown(_serverId); _serverId = 0; throw new InvalidOperationException("copilot_runtime_connection_open failed."); } _sendStream = new CallbackSendStream(SendFrame); }, cancellationToken).ConfigureAwait(false); if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( "FfiRuntimeHost started. Library={Library}, ServerId={ServerId}, ConnectionId={ConnectionId}", _libraryPath, _serverId, _connectionId); } } private static byte[] BuildArgvJson(string? cliEntrypoint, IReadOnlyList args) { using var stream = new MemoryStream(); using (var writer = new Utf8JsonWriter(stream)) { writer.WriteStartArray(); if (cliEntrypoint is not null) { if (cliEntrypoint.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) { writer.WriteStringValue("node"); } writer.WriteStringValue(cliEntrypoint); writer.WriteStringValue("--embedded-host"); writer.WriteStringValue("--no-auto-update"); } foreach (var arg in args) { writer.WriteStringValue(arg); } writer.WriteEndArray(); } return stream.ToArray(); } private static byte[]? BuildEnvJson(IReadOnlyDictionary? environment) { if (environment is null || environment.Count == 0) { return null; } using var stream = new MemoryStream(); using (var writer = new Utf8JsonWriter(stream)) { writer.WriteStartObject(); foreach (var kvp in environment) { writer.WriteString(kvp.Key, kvp.Value); } writer.WriteEndObject(); } return stream.ToArray(); } /// /// Writes one framed message to the native connection. The bytes are read /// synchronously by the native side (it copies before returning), so the /// span does not need to outlive the call — no allocation or copy on our side. /// private delegate bool FrameWriter(ReadOnlySpan frame); private bool SendFrame(ReadOnlySpan frame) { if (_disposed || _connectionId == 0) { return false; } return NativeConnectionWrite(_connectionId, frame); } private void FeedInbound(IntPtr bytesPtr, UIntPtr bytesLen) { var length = checked((int)bytesLen.ToUInt64()); var buffer = new byte[length]; Marshal.Copy(bytesPtr, buffer, 0, length); _receiveStream.Feed(buffer); } public void Dispose() { if (_disposed) { return; } _disposed = true; try { if (_connectionId != 0) { NativeConnectionClose(_connectionId); _connectionId = 0; } } catch (Exception ex) { _logger.LogDebug(ex, "FfiRuntimeHost: connection_close failed"); } try { if (_serverId != 0) { NativeHostShutdown(_serverId); _serverId = 0; } } catch (Exception ex) { _logger.LogDebug(ex, "FfiRuntimeHost: host_shutdown failed"); } _receiveStream.Complete(); DisposeNativeCallback(); } /// Length as the native pointer-sized unsigned integer the ABI expects. private static UIntPtr Len(int value) => new((uint)value); #if NET // ---- Modern interop: source-generated LibraryImport P/Invoke (trim/AOT-safe) ---- private static readonly object ResolverLock = new(); private static bool s_resolverRegistered; private static string? s_resolvedLibraryPath; // A normal (non-pinned) handle to this instance, passed to the native side as // the callback's user_data so the static outbound callback can route back here. private GCHandle _selfHandle; /// /// Registers (once) a process-wide /// that maps to the absolute runtime.node path so the /// stubs resolve. The resolved handle is cached by /// the runtime after first use, so all in-process hosts share a single loaded library. /// private static void PrepareNativeLibrary(string libraryPath) { lock (ResolverLock) { if (s_resolvedLibraryPath is not null && s_resolvedLibraryPath != libraryPath) { throw new InvalidOperationException( $"An in-process FFI runtime library is already loaded from '{s_resolvedLibraryPath}'; " + $"loading a different library from '{libraryPath}' in the same process is not supported."); } s_resolvedLibraryPath = libraryPath; if (!s_resolverRegistered) { NativeLibrary.SetDllImportResolver(typeof(FfiRuntimeHost).Assembly, Resolve); s_resolverRegistered = true; } } } private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { if (libraryName == LibraryName && s_resolvedLibraryPath is not null) { return NativeLibrary.Load(s_resolvedLibraryPath); } return IntPtr.Zero; } private static uint NativeHostStart(byte[] argvJson, byte[]? env) => HostStart(argvJson, Len(argvJson.Length), env, env is null ? UIntPtr.Zero : Len(env.Length)); private uint NativeOpenConnection(uint serverId) { _selfHandle = GCHandle.Alloc(this); unsafe { return ConnectionOpen( serverId, &OnOutboundStatic, GCHandle.ToIntPtr(_selfHandle), null, UIntPtr.Zero, null, UIntPtr.Zero, null, UIntPtr.Zero); } } private static bool NativeHostShutdown(uint serverId) => HostShutdown(serverId); private static bool NativeConnectionWrite(uint connectionId, ReadOnlySpan frame) => ConnectionWrite(connectionId, frame, Len(frame.Length)); private static bool NativeConnectionClose(uint connectionId) => ConnectionClose(connectionId); private void DisposeNativeCallback() { if (_selfHandle.IsAllocated) { _selfHandle.Free(); } } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] private static void OnOutboundStatic(IntPtr userData, IntPtr bytesPtr, nuint bytesLen) { if (userData == IntPtr.Zero || bytesPtr == IntPtr.Zero || bytesLen == 0) { return; } if (GCHandle.FromIntPtr(userData).Target is FfiRuntimeHost self) { self.FeedInbound(bytesPtr, bytesLen); } } [LibraryImport(LibraryName, EntryPoint = "copilot_runtime_host_start")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] private static partial uint HostStart( byte[] argvJson, nuint argvJsonLen, byte[]? env, nuint envLen); [LibraryImport(LibraryName, EntryPoint = "copilot_runtime_host_shutdown")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] [return: MarshalAs(UnmanagedType.U1)] private static partial bool HostShutdown(uint serverId); [LibraryImport(LibraryName, EntryPoint = "copilot_runtime_connection_open")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] private static unsafe partial uint ConnectionOpen( uint serverId, delegate* unmanaged[Cdecl] onOutbound, IntPtr userData, byte[]? extSource, nuint extSourceLen, byte[]? extName, nuint extNameLen, byte[]? connToken, nuint connTokenLen); [LibraryImport(LibraryName, EntryPoint = "copilot_runtime_connection_write")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] [return: MarshalAs(UnmanagedType.U1)] private static partial bool ConnectionWrite(uint connectionId, ReadOnlySpan bytes, nuint bytesLen); [LibraryImport(LibraryName, EntryPoint = "copilot_runtime_connection_close")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] [return: MarshalAs(UnmanagedType.U1)] private static partial bool ConnectionClose(uint connectionId); #else // ---- Legacy interop: delegate-based P/Invoke for netstandard2.0 ---- // netstandard2.0 has neither LibraryImport, NativeLibrary, nor UnmanagedCallersOnly, // so the cdylib is loaded through a hand-rolled dlopen/LoadLibrary shim and each // export is bound to a [UnmanagedFunctionPointer] delegate. The outbound callback is // an instance delegate kept alive in a field for the connection's lifetime. [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint HostStartDelegate( byte[] argvJson, UIntPtr argvJsonLen, byte[]? env, UIntPtr envLen); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] private delegate bool HostShutdownDelegate(uint serverId); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint ConnectionOpenDelegate( uint serverId, OutboundCallbackDelegate onOutbound, IntPtr userData, byte[]? extSource, UIntPtr extSourceLen, byte[]? extName, UIntPtr extNameLen, byte[]? connToken, UIntPtr connTokenLen); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] private delegate bool ConnectionWriteDelegate(uint connectionId, IntPtr bytes, UIntPtr bytesLen); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] private delegate bool ConnectionCloseDelegate(uint connectionId); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void OutboundCallbackDelegate(IntPtr userData, IntPtr bytesPtr, UIntPtr bytesLen); private static readonly object NativeLock = new(); private static bool s_loaded; private static string? s_loadedPath; private static HostStartDelegate? s_hostStart; private static HostShutdownDelegate? s_hostShutdown; private static ConnectionOpenDelegate? s_connectionOpen; private static ConnectionWriteDelegate? s_connectionWrite; private static ConnectionCloseDelegate? s_connectionClose; // Held for the connection's lifetime so the marshaled function pointer handed to the // native side is not collected while Rust may still invoke it. private OutboundCallbackDelegate? _outboundDelegate; private static void PrepareNativeLibrary(string libraryPath) { lock (NativeLock) { if (s_loaded) { if (s_loadedPath != libraryPath) { throw new InvalidOperationException( $"An in-process FFI runtime library is already loaded from '{s_loadedPath}'; " + $"loading a different library from '{libraryPath}' in the same process is not supported."); } return; } var handle = NativeLoader.Load(libraryPath); if (handle == IntPtr.Zero) { throw new InvalidOperationException($"Failed to load FFI runtime library '{libraryPath}'."); } s_hostStart = Bind(handle, "copilot_runtime_host_start"); s_hostShutdown = Bind(handle, "copilot_runtime_host_shutdown"); s_connectionOpen = Bind(handle, "copilot_runtime_connection_open"); s_connectionWrite = Bind(handle, "copilot_runtime_connection_write"); s_connectionClose = Bind(handle, "copilot_runtime_connection_close"); s_loaded = true; s_loadedPath = libraryPath; } } private static T Bind(IntPtr handle, string export) where T : Delegate { var symbol = NativeLoader.GetSymbol(handle, export); if (symbol == IntPtr.Zero) { throw new InvalidOperationException($"FFI runtime library is missing the '{export}' export."); } return Marshal.GetDelegateForFunctionPointer(symbol); } private static uint NativeHostStart(byte[] argvJson, byte[]? env) => s_hostStart!(argvJson, Len(argvJson.Length), env, env is null ? UIntPtr.Zero : Len(env.Length)); private uint NativeOpenConnection(uint serverId) { _outboundDelegate = OnOutbound; return s_connectionOpen!( serverId, _outboundDelegate, IntPtr.Zero, null, UIntPtr.Zero, null, UIntPtr.Zero, null, UIntPtr.Zero); } private static bool NativeHostShutdown(uint serverId) => s_hostShutdown!(serverId); private static unsafe bool NativeConnectionWrite(uint connectionId, ReadOnlySpan frame) { fixed (byte* ptr = frame) { return s_connectionWrite!(connectionId, (IntPtr)ptr, Len(frame.Length)); } } private static bool NativeConnectionClose(uint connectionId) => s_connectionClose!(connectionId); private void DisposeNativeCallback() => _outboundDelegate = null; private void OnOutbound(IntPtr userData, IntPtr bytesPtr, UIntPtr bytesLen) { if (bytesPtr == IntPtr.Zero || bytesLen == UIntPtr.Zero) { return; } FeedInbound(bytesPtr, bytesLen); } /// /// Minimal cross-platform native library loader for netstandard2.0, which lacks /// NativeLibrary. Uses LoadLibrary/GetProcAddress on Windows /// and dlopen/dlsym elsewhere (trying libdl.so.2 first, then /// libdl for older Linux and macOS). /// private static class NativeLoader { public static IntPtr Load(string path) => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Windows.LoadLibrary(path) : Unix.Open(path); public static IntPtr GetSymbol(IntPtr handle, string name) => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Windows.GetProcAddress(handle, name) : Unix.Sym(handle, name); private static class Windows { [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPWStr)] string path); [DllImport("kernel32", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr GetProcAddress(IntPtr module, [MarshalAs(UnmanagedType.LPStr)] string name); } private static class Unix { private const int RtldNow = 2; public static IntPtr Open(string path) { try { return Libdl2.dlopen(path, RtldNow); } catch (DllNotFoundException) { return Libdl1.dlopen(path, RtldNow); } } public static IntPtr Sym(IntPtr handle, string name) { try { return Libdl2.dlsym(handle, name); } catch (DllNotFoundException) { return Libdl1.dlsym(handle, name); } } private static class Libdl2 { [DllImport("libdl.so.2", EntryPoint = "dlopen", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string fileName, int flags); [DllImport("libdl.so.2", EntryPoint = "dlsym", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol); } private static class Libdl1 { [DllImport("libdl", EntryPoint = "dlopen", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string fileName, int flags); [DllImport("libdl", EntryPoint = "dlsym", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol); } } } #endif /// /// A read-only stream fed by the native outbound callback. Chunks are queued on /// an unbounded channel and drained in order by the JSON-RPC read loop. /// private sealed class CallbackReceiveStream : Stream { private readonly Channel _channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true, SingleWriter = false }); private ReadOnlyMemory _leftover; public void Feed(byte[] data) => _channel.Writer.TryWrite(data); public void Complete() => _channel.Writer.TryComplete(); #if !NETSTANDARD2_0 public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { return await ReadCoreAsync(buffer, cancellationToken).ConfigureAwait(false); } #endif private async ValueTask ReadCoreAsync(Memory buffer, CancellationToken cancellationToken) { if (_leftover.IsEmpty) { while (true) { if (!await _channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { return 0; // EOF: channel completed. } if (_channel.Reader.TryRead(out var chunk)) { _leftover = chunk; break; } // Data was signalled but lost a race for it; wait again rather // than reporting a spurious EOF. } } var n = Math.Min(buffer.Length, _leftover.Length); _leftover.Span.Slice(0, n).CopyTo(buffer.Span); _leftover = _leftover.Slice(n); return n; } public override int Read(byte[] buffer, int offset, int count) => ReadCoreAsync(buffer.AsMemory(offset, count), CancellationToken.None).AsTask().GetAwaiter().GetResult(); public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => ReadCoreAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } /// /// A write-only stream that forwards each frame to the native /// connection_write export. /// private sealed class CallbackSendStream(FrameWriter write) : Stream { private void WriteFrame(ReadOnlySpan frame) { if (!write(frame)) { throw new IOException("Failed to write a frame to the in-process runtime connection."); } } public override void Write(byte[] buffer, int offset, int count) => WriteFrame(buffer.AsSpan(offset, count)); #if !NETSTANDARD2_0 public override void Write(ReadOnlySpan buffer) => WriteFrame(buffer); public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { WriteFrame(buffer.Span); return ValueTask.CompletedTask; } #endif public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { WriteFrame(buffer.AsSpan(offset, count)); return Task.CompletedTask; } public override bool CanRead => false; public override bool CanSeek => false; public override bool CanWrite => true; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { } public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); } }