Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new AddEventListener and RemoveEventListener APIs on JSObject #55849

Merged
merged 18 commits into from
Jul 28, 2021
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