Merge nucleic/warm-north-vole-pf37 into dev
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user