Skip to content
5 changes: 5 additions & 0 deletions src/libraries/Common/src/Interop/Browser/Interop.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ internal static partial class Runtime
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern object TypedArrayCopyFrom(int jsObjHandle, int arrayPtr, int begin, int end, int bytesPerElement, out int exceptionalResult);

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern string? AddEventListener(int jsObjHandle, string name, int weakDelegateHandle, int optionsObjHandle);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern string? RemoveEventListener(int jsObjHandle, string name, int weakDelegateHandle, bool capture);

// / <summary>
// / Execute the provided string in the JavaScript context
// / </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@ internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellati
_onError = errorEvt => errorEvt.Dispose();

// Attach the onError callback
_innerWebSocket.SetObjectProperty("onerror", _onError);
_innerWebSocket.AddEventListener("error", _onError);

// Setup the onClose callback
_onClose = (closeEvent) => OnCloseCallback(closeEvent, cancellationToken);

// Attach the onClose callback
_innerWebSocket.SetObjectProperty("onclose", _onClose);
_innerWebSocket.AddEventListener("close", _onClose);

// Setup the onOpen callback
_onOpen = (evt) =>
Expand Down Expand Up @@ -203,13 +203,13 @@ internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellati
};

// Attach the onOpen callback
_innerWebSocket.SetObjectProperty("onopen", _onOpen);
_innerWebSocket.AddEventListener("open", _onOpen);

// Setup the onMessage callback
_onMessage = (messageEvent) => OnMessageCallback(messageEvent);

// Attach the onMessage callaback
_innerWebSocket.SetObjectProperty("onmessage", _onMessage);
_innerWebSocket.AddEventListener("message", _onMessage);
await _tcsConnect.Task.ConfigureAwait(continueOnCapturedContext: true);
}
catch (Exception wse)
Expand Down Expand Up @@ -298,7 +298,7 @@ private void OnMessageCallback(JSObject messageEvent)
}
}
};
reader.Invoke("addEventListener", "loadend", loadend);
reader.AddEventListener("loadend", loadend);
reader.Invoke("readAsArrayBuffer", blobData);
}
break;
Expand All @@ -318,26 +318,10 @@ private void NativeCleanup()
{
// We need to clear the events on websocket as well or stray events
// are possible leading to crashes.
if (_onClose != null)
{
_innerWebSocket?.SetObjectProperty("onclose", "");
_onClose = null;
}
if (_onError != null)
{
_innerWebSocket?.SetObjectProperty("onerror", "");
_onError = null;
}
if (_onOpen != null)
{
_innerWebSocket?.SetObjectProperty("onopen", "");
_onOpen = null;
}
if (_onMessage != null)
{
_innerWebSocket?.SetObjectProperty("onmessage", "");
_onMessage = null;
}
_innerWebSocket?.RemoveEventListener("close", _onClose);
_innerWebSocket?.RemoveEventListener("error", _onError);
_innerWebSocket?.RemoveEventListener("open", _onOpen);
_innerWebSocket?.RemoveEventListener("message", _onMessage);
}

public override void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,61 @@ public object Invoke(string method, params object?[] args)
return res;
}

public struct EventListenerOptions {
public bool Capture;
public bool Once;
public bool Passive;
public object? Signal;
}

public int AddEventListener(string name, Delegate listener, EventListenerOptions? options = null)
{
var optionsDict = options.HasValue
? new JSObject()
: null;

try {
if (options?.Signal != null)
throw new NotImplementedException("EventListenerOptions.Signal");

var jsfunc = Runtime.GetJSOwnedObjectHandle(listener);
// int exception;
if (options.HasValue) {
// TODO: Optimize this
var _options = options.Value;
optionsDict?.SetObjectProperty("capture", _options.Capture, true, true);
optionsDict?.SetObjectProperty("once", _options.Once, true, true);
optionsDict?.SetObjectProperty("passive", _options.Passive, true, true);
}

// TODO: Pass options explicitly instead of using the object
// TODO: Handle errors
// We can't currently do this because adding any additional parameters or a return value causes
// a signature mismatch at runtime
var ret = Interop.Runtime.AddEventListener(JSHandle, name, jsfunc, optionsDict?.JSHandle ?? 0);
if (ret != null)
throw new JSException(ret);
return jsfunc;
} finally {
optionsDict?.Dispose();
}
}

public void RemoveEventListener(string name, Delegate? listener, EventListenerOptions? options = null)
{
if (listener == null)
return;
var jsfunc = Runtime.GetJSOwnedObjectHandle(listener);
RemoveEventListener(name, jsfunc, options);
}

public void RemoveEventListener(string name, int listenerHandle, EventListenerOptions? options = null)
{
var ret = Interop.Runtime.RemoveEventListener(JSHandle, name, listenerHandle, options?.Capture ?? false);
if (ret != null)
throw new JSException(ret);
}

/// <summary>
/// Returns the named property from the object, or throws a JSException on error.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,74 @@ public static int BindExistingObject(object rawObj, int jsId)
return jsObject.Int32Handle;
}

private static int NextJSOwnedObjectID = 1;
private static object JSOwnedObjectLock = new object();
private static Dictionary<object, int> IDFromJSOwnedObject = new Dictionary<object, int>();
private static Dictionary<int, object> JSOwnedObjectFromID = new Dictionary<int, object>();

// A JSOwnedObject is a managed object with its lifetime controlled by javascript.
// The managed side maintains a strong reference to the object, while the JS side
// maintains a weak reference and notifies the managed side if the JS wrapper object
// has been reclaimed by the JS GC. At that point, the managed side will release its
// strong references, allowing the managed object to be collected.
// This ensures that things like delegates and promises will never 'go away' while JS
// is expecting to be able to invoke or await them.
public static int GetJSOwnedObjectHandle (object o) {
if (o == null)
return 0;

int result;
lock (JSOwnedObjectLock) {
if (IDFromJSOwnedObject.TryGetValue(o, out result))
return result;

result = NextJSOwnedObjectID++;
IDFromJSOwnedObject[o] = result;
JSOwnedObjectFromID[result] = o;
return result;
}
}

// The JS layer invokes this method when the JS wrapper for a JS owned object
// has been collected by the JS garbage collector
public static void ReleaseJSOwnedObjectByHandle (int id) {
lock (JSOwnedObjectLock) {
if (!JSOwnedObjectFromID.TryGetValue(id, out object? o))
throw new Exception($"JS-owned object with id {id} was already released");
IDFromJSOwnedObject.Remove(o);
JSOwnedObjectFromID.Remove(id);
}
}

// The JS layer invokes this API when the JS wrapper for a delegate is invoked.
// In multiple places this function intentionally returns false instead of throwing
// in an unexpected condition. This is done because unexpected conditions of this
// type are usually caused by a JS object (i.e. a WebSocket) receiving an event
// after its managed owner has been disposed - throwing in that case is unwanted.
public static bool TryInvokeJSOwnedDelegateByHandle (int id, JSObject? arg1) {
Delegate? del;
lock (JSOwnedObjectLock) {
if (!JSOwnedObjectFromID.TryGetValue(id, out object? o))
return false;
del = (Delegate)o;
}

if (del == null)
return false;

// error CS0117: 'Array' does not contain a definition for 'Empty' [/home/kate/Projects/dotnet-runtime-wasm/src/libraries/System.Private.Runtime.InteropServices.JavaScript/src/System.Private.Runtime.InteropServices.JavaScript.csproj]
#pragma warning disable CA1825

if (arg1 != null)
del.DynamicInvoke(new object[] { arg1 });
else
del.DynamicInvoke(new object[0]);

#pragma warning restore CA1825

return true;
}

public static int GetJSObjectId(object rawObj)
{
JSObject? jsObject;
Expand Down
Loading