140 lines
4.9 KiB
C#
140 lines
4.9 KiB
C#
using System.Runtime.InteropServices;
|
|
using System.Text.Json;
|
|
|
|
namespace NucleicProtocol.Interop;
|
|
|
|
/// Managed lifetime and callback barrier over one `np_handle`.
|
|
public interface IProtocolClient : IDisposable
|
|
{
|
|
event Action<string>? MessageJsonReceived;
|
|
event Action<string>? StateJsonReceived;
|
|
ProtocolResult ConnectLocal(HostdRendezvous rendezvous);
|
|
ProtocolResult ConnectRemote(string endpointJson);
|
|
ProtocolResult Pair(string pairingCode);
|
|
ProtocolResult SendIntent(string clientMessageJson);
|
|
}
|
|
|
|
public sealed class ProtocolClient : IProtocolClient
|
|
{
|
|
private static readonly NativeProtocolCallback EventThunk = OnNativeEvent;
|
|
private static readonly NativeProtocolCallback StateThunk = OnNativeState;
|
|
|
|
private readonly object gate = new();
|
|
private readonly INativeProtocol native;
|
|
private nint handle;
|
|
private GCHandle callbackContext;
|
|
private bool disposed;
|
|
|
|
public event Action<string>? MessageJsonReceived;
|
|
public event Action<string>? StateJsonReceived;
|
|
|
|
public ProtocolClient(
|
|
ProtocolClientConfiguration configuration, INativeProtocol? native = null)
|
|
{
|
|
this.native = native ?? PInvokeNativeProtocol.Instance;
|
|
var json = JsonSerializer.Serialize(
|
|
configuration, ProtocolJsonContext.Default.ProtocolClientConfiguration);
|
|
handle = this.native.ClientCreate(json);
|
|
if (handle == 0) throw new InvalidOperationException("NucleicProtocolC rejected client configuration");
|
|
|
|
callbackContext = GCHandle.Alloc(this, GCHandleType.Normal);
|
|
try
|
|
{
|
|
this.native.ClientSetCallbacks(
|
|
handle, EventThunk, StateThunk, GCHandle.ToIntPtr(callbackContext));
|
|
}
|
|
catch
|
|
{
|
|
this.native.ClientClose(handle);
|
|
handle = 0;
|
|
callbackContext.Free();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public ProtocolResult ConnectLocal(HostdRendezvous rendezvous)
|
|
{
|
|
rendezvous.Validate();
|
|
return Invoke(handle => native.ClientConnectLocal(
|
|
handle, JsonSerializer.Serialize(rendezvous, ProtocolJsonContext.Default.HostdRendezvous)));
|
|
}
|
|
|
|
public ProtocolResult ConnectRemote(string endpointJson) =>
|
|
Invoke(handle => native.ClientConnectRemote(handle, endpointJson));
|
|
|
|
public ProtocolResult Pair(string pairingCode) =>
|
|
Invoke(handle => native.ClientPair(handle, pairingCode));
|
|
|
|
public ProtocolResult SendIntent(string clientMessageJson)
|
|
{
|
|
// Fail malformed UI output before it reaches the ABI. The Swift decoder remains the
|
|
// authority for the actual ClientMsg shape and returns DecodeError for a valid-but-wrong
|
|
// document.
|
|
using (JsonDocument.Parse(clientMessageJson)) { }
|
|
return Invoke(handle => native.ClientSendIntent(handle, clientMessageJson));
|
|
}
|
|
|
|
private ProtocolResult Invoke(Func<nint, int> operation)
|
|
{
|
|
lock (gate)
|
|
{
|
|
ObjectDisposedException.ThrowIf(disposed, this);
|
|
return (ProtocolResult)operation(handle);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
nint closing;
|
|
lock (gate)
|
|
{
|
|
if (disposed) return;
|
|
disposed = true;
|
|
closing = handle;
|
|
handle = 0;
|
|
}
|
|
|
|
// The Swift contract makes close a callback barrier. Only after it returns is it safe to
|
|
// free the GCHandle that native callbacks carry as their opaque context.
|
|
native.ClientClose(closing);
|
|
MessageJsonReceived = null;
|
|
StateJsonReceived = null;
|
|
if (callbackContext.IsAllocated) callbackContext.Free();
|
|
}
|
|
|
|
private static void OnNativeEvent(nint context, nint utf8Json) =>
|
|
Dispatch(context, utf8Json, static (client, json) => client.MessageJsonReceived?.Invoke(json));
|
|
|
|
private static void OnNativeState(nint context, nint utf8Json) =>
|
|
Dispatch(context, utf8Json, static (client, json) => client.StateJsonReceived?.Invoke(json));
|
|
|
|
private static void Dispatch(
|
|
nint context, nint utf8Json, Action<ProtocolClient, string> deliver)
|
|
{
|
|
if (context == 0 || utf8Json == 0) return;
|
|
try
|
|
{
|
|
if (GCHandle.FromIntPtr(context).Target is ProtocolClient client)
|
|
{
|
|
var json = Marshal.PtrToStringUTF8(utf8Json);
|
|
if (json is not null) deliver(client, json);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Nothing may unwind through the C callback boundary. Renderer event subscribers own
|
|
// their diagnostics; a bad observer must not tear down Swift's callback thread.
|
|
}
|
|
}
|
|
}
|
|
|
|
public interface IProtocolClientFactory
|
|
{
|
|
IProtocolClient Create(ProtocolClientConfiguration configuration);
|
|
}
|
|
|
|
public sealed class ProtocolClientFactory : IProtocolClientFactory
|
|
{
|
|
public IProtocolClient Create(ProtocolClientConfiguration configuration) => new ProtocolClient(configuration);
|
|
}
|