Skip to content

Commit

Permalink
JS Reference API improvements (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored Jul 16, 2024
1 parent bb0192a commit bde1451
Show file tree
Hide file tree
Showing 21 changed files with 197 additions and 74 deletions.
2 changes: 1 addition & 1 deletion bench/Benchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public void CallDotnetMethodWithArgs()
[Benchmark]
public void ReferenceGet()
{
_ = _reference.GetValue()!.Value;
_ = _reference.GetValue();
}

[Benchmark]
Expand Down
37 changes: 22 additions & 15 deletions docs/features/js-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ The [`JSReference`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference
or weak reference to a JavaScript value. Use a reference to save a JS value on the .NET heap and
enable it to be accessed later from a different [scope](./js-value-scopes).

::: warning
The example code below might need to be updated after
https://github.com/microsoft/node-api-dotnet/issues/197 is resolved.
:::

## Using strong references

A common practice is to save a reference as a member variable to support using the referenced
JS value in a later callback. A strong reference prevents the JS value from being released
until the reference is disposed.
until the reference is disposed. The referenced value can be retrieved later via the
[`JSReference.GetValue()`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference/GetValue)
method.

```C#
[JSExport]
Expand All @@ -23,17 +20,17 @@ public class ReferenceExample : IDisposable

public ReferenceExample(JSArray data)
{
// The constructor must have been invoked from JS, or from .NET
// on the JS thread. Save a reference to the JS value parameter.
// The constructor must have been invoked from JS, or from .NET on the JS thread.
// Save a reference to the JS value parameter. DO NOT store the JSArray directly
// as a member because it will be invalid as soon as this method returns.
_dataReference = new JSReference(data);
}

public double GetSum()
{
// Access the saved data value via the reference.
// Since the reference is strong, it never returns null.
// (It throws ObjectDisposedException if disposed.)
JSArray data = (JSArray)_dataReference.GetValue()!.Value;
// (It throws ObjectDisposedException if the reference is disposed.)
JSArray data = (JSArray)_dataReference.GetValue();

// JSArray implements IList<JSValue>.
return data.Sum((JSValue value) => (double)value);
Expand All @@ -50,12 +47,12 @@ public class ReferenceExample : IDisposable

## Using weak references

A weak reference does not prevent the JS value from being released. Therefore it is
necessary to check for null when getting the referenced value:
A weak reference does not prevent the JS value from being released. Use
[`JSReference.TryGetValue(out JSValue)`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference/TryGetValue)
to conditionally retrieve a weakly-referenced value if it is still available.

```C#
JSValue? value = reference.GetValue();
if (value != null)
if (weakReference.TryGetValue(out JSValue value))
{
// Do something with the value.
}
Expand All @@ -64,3 +61,13 @@ else
// The JS value was released and is no longer available.
}
```

## Reference limitations

Currently only values of type `object`, `function`, and `symbol` can be referenced. It is not
possible to create a reference to other value types such as `string`, `number`, `boolean`, or
`undefined`.

If the type of a value to be referenced is not known, use
[`JSReference.TryCreateReference()`](../reference/dotnet/Microsoft.JavaScript.NodeApi/JSReference/TryCreateReference)
and check the return value.
8 changes: 3 additions & 5 deletions src/NodeApi.DotNetHost/JSMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ private LambdaExpression BuildToJSFunctionExpression(Type delegateType)
* var __valueRef = new JSReference(__value);
* var __syncContext = JSSynchronizationContext.Current;
* return (...args) => __syncContext.Run(() =>
* __valueRef.GetValue().Value.Call(...args));
* __valueRef.GetValue().Call(...args));
*/
ParameterExpression valueParameter = Expression.Parameter(typeof(JSValue), "__value");
ParameterExpression valueRefVariable = Expression.Variable(
Expand All @@ -912,11 +912,9 @@ private LambdaExpression BuildToJSFunctionExpression(Type delegateType)
syncContextVariable,
Expression.Property(null, typeof(JSSynchronizationContext).GetStaticProperty(
nameof(JSSynchronizationContext.Current))));
Expression getValueExpression = Expression.Property(
Expression.Call(
Expression getValueExpression = Expression.Call(
valueRefVariable,
typeof(JSReference).GetInstanceMethod(nameof(JSReference.GetValue))),
"Value");
typeof(JSReference).GetInstanceMethod(nameof(JSReference.GetValue)));

ParameterExpression[] parameters = invokeMethod.GetParameters()
.Select(Parameter).ToArray();
Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi.DotNetHost/ManagedHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ public JSValue LoadModule(JSCallbackArgs args)
if (_loadedModules.TryGetValue(assemblyFilePath, out JSReference? exportsRef))
{
Trace("< ManagedHost.LoadModule() => already loaded");
return exportsRef.GetValue()!.Value;
return exportsRef.GetValue();
}

Assembly assembly;
Expand Down
4 changes: 2 additions & 2 deletions src/NodeApi.DotNetHost/NamespaceProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private JSProxy GetNamespaceObject()
{
if (_valueReference != null)
{
return (JSProxy)_valueReference.GetValue()!.Value;
return (JSProxy)_valueReference.GetValue();
}

JSProxy proxy = new(new JSObject(), CreateProxyHandler());
Expand All @@ -102,7 +102,7 @@ private JSFunction GetToStringFunction()
{
if (_tostringReference != null)
{
return (JSFunction)_tostringReference.GetValue()!.Value;
return (JSFunction)_tostringReference.GetValue();
}

// Calling `toString()` on a namespace returns the full namespace name.
Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi.DotNetHost/TypeExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void ExportAssemblyTypes(Assembly assembly)
if (_namespaces != null)
{
// Add a property on the namespaces JS object.
JSObject namespacesObject = (JSObject)_namespaces.GetValue()!.Value;
JSObject namespacesObject = (JSObject)_namespaces.GetValue();
namespacesObject[namespaceParts[0]] = parentNamespace.Value;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi/DotNetHost/NativeHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private JSValue InitializeManagedHost(JSCallbackArgs args)
// Normally this shouldn't happen because the host package initialization
// script would only be loaded once by require(). But certain situations like
// drive letter or path casing inconsistencies can cause it to be loaded twice.
return _exports.GetValue()!.Value;
return _exports.GetValue();
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi/Interop/JSAbortSignal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private static JSAbortSignal FromCancellationToken(CancellationToken cancellatio
JSSynchronizationContext syncContext = JSSynchronizationContext.Current!;
cancellation.Register(() => syncContext.Post(() =>
{
controllerReference.GetValue()!.Value.CallMethod("abort");
controllerReference.GetValue().CallMethod("abort");
controllerReference.Dispose();
}));
return new JSAbortSignal(controller["signal"]);
Expand Down
4 changes: 2 additions & 2 deletions src/NodeApi/Interop/JSCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ internal JSIterableEnumerable(JSValue iterable, JSValue.To<T> fromJS)

private readonly JSReference _iterableReference;

internal JSValue Value => _iterableReference.GetValue()!.Value;
internal JSValue Value => _iterableReference.GetValue();

protected void Run(Action<JSValue> action) => _iterableReference.Run(action);
protected TResult Run<TResult>(Func<JSValue, TResult> action) => _iterableReference.Run(action);
Expand Down Expand Up @@ -664,7 +664,7 @@ internal JSMapReadOnlyDictionary(
protected void Run(Action<JSValue> action) => _mapReference.Run(action);
protected TResult Run<TResult>(Func<JSValue, TResult> action) => _mapReference.Run(action);

internal JSValue Value => _mapReference.GetValue()!.Value;
internal JSValue Value => _mapReference.GetValue();

bool IEquatable<JSValue>.Equals(JSValue other) => Run((map) => map.Equals(other));

Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi/Interop/JSInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected JSInterface(JSValue value)
/// <summary>
/// Gets the underlying JS value.
/// </summary>
protected JSValue Value => ValueReference.GetValue()!.Value;
protected JSValue Value => ValueReference.GetValue();

/// <summary>
/// Dynamically invokes an interface method JS adapter delegate after obtaining the JS `this`
Expand Down
8 changes: 5 additions & 3 deletions src/NodeApi/Interop/JSRuntimeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ internal JSValue InitializeObjectWrapper(JSValue wrapper, JSValue externalInstan

if (_objectMap.TryGetValue(obj, out JSReference? existingWrapperWeakRef))
{
if (!existingWrapperWeakRef.IsDisposed && existingWrapperWeakRef.GetValue().HasValue)
if (!existingWrapperWeakRef.IsDisposed && existingWrapperWeakRef.TryGetValue(out _))
{
// If the .NET object is already mapped to a non-released JS object, then
// another one should not be created.
Expand Down Expand Up @@ -626,6 +626,9 @@ public JSValue Import(

JSReference reference = _importMap.GetOrAdd((module, property), (_) =>
{
// TODO: Consider what to do if the imported property is undefined
// or not a referenceable type.
if (module == null || module == "global")
{
// Importing a built-in object from `global`.
Expand Down Expand Up @@ -656,9 +659,8 @@ public JSValue Import(
return new JSReference(propertyValue);
}
}
});
return reference.GetValue() ?? JSValue.Undefined;
return reference.GetValue();
}

/// <summary>
Expand Down
18 changes: 9 additions & 9 deletions src/NodeApi/Interop/NodeStream.Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static JSValue GetOrCreateAdapter(bool readable, bool writable)
{
if (s_duplexStreamAdapterReference != null)
{
return s_duplexStreamAdapterReference.GetValue()!.Value;
return s_duplexStreamAdapterReference.GetValue();
}

var streamAdapter = new JSObject
Expand All @@ -116,7 +116,7 @@ private static JSValue GetOrCreateAdapter(bool readable, bool writable)
{
if (s_readableStreamAdapterReference != null)
{
return s_readableStreamAdapterReference.GetValue()!.Value;
return s_readableStreamAdapterReference.GetValue();
}

var streamAdapter = new JSObject
Expand All @@ -132,7 +132,7 @@ private static JSValue GetOrCreateAdapter(bool readable, bool writable)
{
if (s_writableStreamAdapterReference != null)
{
return s_writableStreamAdapterReference.GetValue()!.Value;
return s_writableStreamAdapterReference.GetValue();
}

var streamAdapter = new JSObject
Expand Down Expand Up @@ -181,15 +181,15 @@ private static async void ReadAsync(
int count = await stream.ReadAsync(buffer, 0, ReadChunkSize);
#endif

nodeStream = nodeStreamReference.GetValue()!.Value;
nodeStream = nodeStreamReference.GetValue();
nodeStream.CallMethod(
"push", count == 0 ? JSValue.Null : new JSTypedArray<byte>(buffer, 0, count));
}
catch (Exception ex)
{
try
{
nodeStream = nodeStreamReference.GetValue()!.Value;
nodeStream = nodeStreamReference.GetValue();
nodeStream.CallMethod("destroy", new JSError(ex).Value);
}
catch (Exception)
Expand Down Expand Up @@ -253,15 +253,15 @@ private static async void WriteAsync(
await stream.WriteAsync(chunk.ToArray(), 0, chunk.Length);
#endif

callback = callbackReference.GetValue()!.Value;
callback = callbackReference.GetValue();
callback.Call();
}
catch (Exception ex)
{
bool isExceptionPending5 = JSError.IsExceptionPending();
try
{
callback = callbackReference.GetValue()!.Value;
callback = callbackReference.GetValue();
callback.Call(thisArg: JSValue.Undefined, new JSError(ex).Value);
}
catch (Exception)
Expand Down Expand Up @@ -292,14 +292,14 @@ private static async void FinalAsync(
{
await stream.FlushAsync();

callback = callbackReference.GetValue()!.Value;
callback = callbackReference.GetValue();
callback.Call();
}
catch (Exception ex)
{
try
{
callback = callbackReference.GetValue()!.Value;
callback = callbackReference.GetValue();
callback.Call(thisArg: JSValue.Undefined, new JSError(ex).Value);
}
catch (Exception)
Expand Down
5 changes: 2 additions & 3 deletions src/NodeApi/Interop/NodeStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public partial class NodeStream : Stream

public static explicit operator NodeStream(JSValue value) => new(value);
public static implicit operator JSValue(NodeStream stream)
=> stream._valueReference.GetValue() ?? default;
=> stream._valueReference.GetValue();

private NodeStream(JSValue value)
{
Expand Down Expand Up @@ -53,8 +53,7 @@ private NodeStream(JSValue value)
}));
}

private JSValue Value => _valueReference.GetValue() ??
throw new ObjectDisposedException(nameof(NodeStream));
private JSValue Value => _valueReference.GetValue();

public override bool CanRead => Value.HasProperty("read");

Expand Down
2 changes: 1 addition & 1 deletion src/NodeApi/JSError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public string Message
{
try
{
_message = (string?)_errorRef.GetValue()?["message"];
_message = (string?)_errorRef.GetValue()["message"];
}
catch
{
Expand Down
Loading

0 comments on commit bde1451

Please sign in to comment.