diff --git a/Generator/AdapterGenerator.cs b/Generator/AdapterGenerator.cs index 45a58d7d..a20b4f18 100644 --- a/Generator/AdapterGenerator.cs +++ b/Generator/AdapterGenerator.cs @@ -21,6 +21,7 @@ internal class AdapterGenerator : SourceGenerator private readonly Dictionary _adaptedMembers = new(); private readonly Dictionary _adaptedStructs = new(); + private readonly Dictionary _adaptedArrays = new(); internal AdapterGenerator(GeneratorExecutionContext context) { @@ -132,6 +133,19 @@ private string GetStructAdapterName(ITypeSymbol structType, bool toJS) return (getAdapterName, setAdapterName); } + private string GetArrayAdapterName(ITypeSymbol elementType, bool toJS) + { + string ns = GetNamespace(elementType); + string elementName = elementType.Name; + string prefix = toJS ? AdapterFromPrefix : AdapterToPrefix; + string adapterName = $"{prefix}{ns.Replace('.', '_')}_{elementName}_Array"; + if (!_adaptedArrays.ContainsKey(adapterName)) + { + _adaptedArrays.Add(adapterName, elementType); + } + return adapterName; + } + internal void GenerateAdapters(SourceBuilder s) { foreach (KeyValuePair nameAndSymbol in _adaptedMembers) @@ -164,6 +178,14 @@ internal void GenerateAdapters(SourceBuilder s) ITypeSymbol structSymbol = nameAndSymbol.Value; GenerateStructAdapter(ref s, adapterName, structSymbol); } + + foreach (KeyValuePair nameAndSymbol in _adaptedArrays) + { + s++; + string adapterName = nameAndSymbol.Key; + ITypeSymbol elementSymbol = nameAndSymbol.Value; + GenerateArrayAdapter(ref s, adapterName, elementSymbol); + } } private void GenerateConstructorAdapter( @@ -345,6 +367,59 @@ private void GenerateStructAdapter( } } + private void GenerateArrayAdapter( + ref SourceBuilder s, + string adapterName, + ITypeSymbol elementType) + { + string ns = GetNamespace(elementType); + string elementName = elementType.Name; + + if (adapterName.StartsWith(AdapterFromPrefix)) + { + s += $"private static JSValue {adapterName}({ns}.{elementName}[] array)"; + s += "{"; + s += "JSArray jsArray = new JSArray(array.Length);"; + s += "for (int i = 0; i < array.Length; i++)"; + s += "{"; + s += $"jsArray[i] = {Convert("array[i]", elementType, null)};"; + s += "}"; + s += "return jsArray;"; + s += "}"; + } + else + { + s += $"private static {ns}.{elementName}[] {adapterName}(JSValue value)"; + s += "{"; + s += "JSArray jsArray = (JSArray)value;"; + s += $"{ns}.{elementName}[] array = new {ns}.{elementName}[jsArray.Length];"; + s += "for (int i = 0; i < array.Length; i++)"; + s += "{"; + s += $"array[i] = {Convert("jsArray[i]", null, elementType)};"; + s += "}"; + s += "return array;"; + s += "}"; + } + } + + private bool IsTypedArrayType(ITypeSymbol elementType) + { + return elementType.SpecialType switch + { + SpecialType.System_SByte => true, + SpecialType.System_Byte => true, + SpecialType.System_Int16 => true, + SpecialType.System_UInt16 => true, + SpecialType.System_Int32 => true, + SpecialType.System_UInt32 => true, + SpecialType.System_Int64 => true, + SpecialType.System_UInt64 => true, + SpecialType.System_Single => true, + SpecialType.System_Double => true, + _ => false, + }; + } + private void AdaptThisArg(ref SourceBuilder s, ISymbol symbol) { @@ -425,6 +500,14 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol } else if (toType.TypeKind == TypeKind.Struct) { + if (toType is INamedTypeSymbol namedType && + namedType.TypeParameters.Length == 1 && + namedType.OriginalDefinition.Name == "Memory" && + IsTypedArrayType(namedType.TypeArguments[0])) + { + return $"((JSTypedArray<{namedType.TypeArguments[0]}>){fromExpression}).AsMemory()"; + } + VerifyReferencedTypeIsExported(toType); string adapterName = GetStructAdapterName(toType, toJS: false); @@ -438,6 +521,26 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol return $"{adapterName}({fromExpression})"; } } + else if (toType.TypeKind == TypeKind.Array) + { + ITypeSymbol elementType = ((IArrayTypeSymbol)toType).ElementType; + VerifyReferencedTypeIsExported(elementType); + + string adapterName = GetArrayAdapterName(elementType, toJS: false); + if (isNullable) + { + return $"({fromExpression}).IsNullOrUndefined() ? ({elementType}[]?)null : " + + $"{adapterName}({fromExpression})"; + } + else + { + return $"{adapterName}({fromExpression})"; + } + } + else if (toType is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0) + { + // TODO: Handle generic collections. + } // TODO: Handle other kinds of conversions from JSValue. // TODO: Handle unwrapping external values. @@ -483,6 +586,14 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol } else if (fromType.TypeKind == TypeKind.Struct) { + if (fromType is INamedTypeSymbol namedType && + namedType.TypeParameters.Length == 1 && + namedType.OriginalDefinition.Name == "Memory" && + IsTypedArrayType(namedType.TypeArguments[0])) + { + return $"new JSTypedArray<{namedType.TypeArguments[0]}>({fromExpression})"; + } + VerifyReferencedTypeIsExported(fromType); string adapterName = GetStructAdapterName(fromType, toJS: true); @@ -496,6 +607,26 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol return $"{adapterName}({fromExpression})"; } } + else if (fromType.TypeKind == TypeKind.Array) + { + ITypeSymbol elementType = ((IArrayTypeSymbol)fromType).ElementType; + VerifyReferencedTypeIsExported(elementType); + + string adapterName = GetArrayAdapterName(elementType, toJS: true); + if (isNullable) + { + return $"{fromExpression} == null ? JSValue.Null : " + + $"{adapterName}({fromExpression})"; + } + else + { + return $"{adapterName}({fromExpression})"; + } + } + else if (fromType is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0) + { + // TODO: Handle generic collections. + } // TODO: Handle other kinds of conversions to JSValue. // TODO: Consider wrapping unsupported types in a value of type "external". @@ -512,6 +643,13 @@ private string Convert(string fromExpression, ITypeSymbol? fromType, ITypeSymbol private void VerifyReferencedTypeIsExported(ITypeSymbol type) { + switch (type.SpecialType) + { + case SpecialType.System_Object: + case SpecialType.System_String: return; + default: break; + } + if (ModuleGenerator.GetJSExportAttribute(type) == null) { // TODO: Consider an option to automatically export referenced classes? diff --git a/Generator/ModuleGenerator.cs b/Generator/ModuleGenerator.cs index 2588a4f1..1fe9874d 100644 --- a/Generator/ModuleGenerator.cs +++ b/Generator/ModuleGenerator.cs @@ -57,8 +57,8 @@ public void Execute(GeneratorExecutionContext context) // No type definitions are generated when using a custom init function. if (moduleInitializer is not IMethodSymbol) { - SourceText typeDefinitions = TypeDefinitionsGenerator.GenerateTypeDefinitions( - exportItems); + TypeDefinitionsGenerator tsGenerator = new(exportItems); + SourceText typeDefinitions = tsGenerator.GenerateTypeDefinitions(); if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( "build_property.TargetPath", out string? targetPath)) { diff --git a/Generator/TypeDefinitionsGenerator.cs b/Generator/TypeDefinitionsGenerator.cs index 15c54657..5b773f9e 100644 --- a/Generator/TypeDefinitionsGenerator.cs +++ b/Generator/TypeDefinitionsGenerator.cs @@ -8,19 +8,29 @@ namespace NodeApi.Generator; +// An analyzer bug results in incorrect reports of CA1822 against methods in this class. +#pragma warning disable CA1822 // Mark members as static + internal class TypeDefinitionsGenerator : SourceGenerator { private static readonly Regex s_newlineRegex = new("\n *"); private static readonly Regex s_summaryRegex = new("(.*)"); private static readonly Regex s_remarksRegex = new("(.*)"); - internal static SourceText GenerateTypeDefinitions(IEnumerable exportItems) + private readonly IEnumerable _exportItems; + + public TypeDefinitionsGenerator(IEnumerable exportItems) + { + _exportItems = exportItems; + } + + internal SourceText GenerateTypeDefinitions() { var s = new SourceBuilder(); s += "// Generated type definitions for .NET module"; - foreach (ISymbol exportItem in exportItems) + foreach (ISymbol exportItem in _exportItems) { if (exportItem is ITypeSymbol exportType && (exportType.TypeKind == TypeKind.Class || exportType.TypeKind == TypeKind.Struct)) @@ -50,7 +60,7 @@ internal static SourceText GenerateTypeDefinitions(IEnumerable exportIt return s; } - private static void GenerateClassTypeDefinitions(ref SourceBuilder s, ITypeSymbol exportClass) + private void GenerateClassTypeDefinitions(ref SourceBuilder s, ITypeSymbol exportClass) { s++; GenerateDocComments(ref s, exportClass); @@ -117,8 +127,10 @@ member is IMethodSymbol exportConstructor && s += "}"; } - private static string GetTSType(ITypeSymbol type) + private string GetTSType(ITypeSymbol type) { + string tsType = "unknown"; + string? specialType = type.SpecialType switch { SpecialType.System_Void => "void", @@ -137,25 +149,93 @@ private static string GetTSType(ITypeSymbol type) ////SpecialType.System_DateTime => "Date", _ => null, }; + if (specialType != null) { - return specialType; + tsType = specialType; } - - if (type.TypeKind == TypeKind.Class) + else if (type.TypeKind == TypeKind.Array) { - // TODO: Check if class is exported. + ITypeSymbol elementType = ((IArrayTypeSymbol)type).ElementType; + tsType = GetTSType(elementType) + "[]"; } - else if (type.TypeKind == TypeKind.Array) + else if (type is INamedTypeSymbol namedType && namedType.TypeParameters.Length > 0) + { + if (namedType.OriginalDefinition.Name == "Nullable") + { + tsType = GetTSType(namedType.TypeArguments[0]) + " | null"; + } + else if (namedType.OriginalDefinition.Name == "Memory") + { + ITypeSymbol elementType = namedType.TypeArguments[0]; + tsType = elementType.SpecialType switch + { + SpecialType.System_SByte => "Int8Array", + SpecialType.System_Int16 => "Int16Array", + SpecialType.System_Int32 => "Int32Array", + SpecialType.System_Int64 => "BigInt64Array", + SpecialType.System_Byte => "Uint8Array", + SpecialType.System_UInt16 => "Uint16Array", + SpecialType.System_UInt32 => "Uint32Array", + SpecialType.System_UInt64 => "BigUint64Array", + SpecialType.System_Single => "Float32Array", + SpecialType.System_Double => "Float64Array", + _ => "unknown", + }; + } + else if (namedType.OriginalDefinition.Name == "IList") + { + tsType = GetTSType(namedType.TypeArguments[0]) + "[]"; + } + else if (namedType.OriginalDefinition.Name == "IReadOnlyList") + { + tsType = "readonly " + GetTSType(namedType.TypeArguments[0]) + "[]"; + } + else if (namedType.OriginalDefinition.Name == "ICollection" || + namedType.OriginalDefinition.Name == "ISet") + { + string elementTsType = GetTSType(namedType.TypeArguments[0]); + return $"Set<{elementTsType}>"; + } + else if (namedType.OriginalDefinition.Name == "IReadOnlyCollection" || + namedType.OriginalDefinition.Name == "IReadOnlySet") + { + string elementTsType = GetTSType(namedType.TypeArguments[0]); + return $"ReadonlySet<{elementTsType}>"; + } + else if (namedType.OriginalDefinition.Name == "IEnumerable") + { + string elementTsType = GetTSType(namedType.TypeArguments[0]); + return $"Iterable<{elementTsType}>"; + } + else if (namedType.OriginalDefinition.Name == "IDictionary") + { + string keyTSType = GetTSType(namedType.TypeArguments[0]); + string valueTSType = GetTSType(namedType.TypeArguments[1]); + tsType = $"Map<{keyTSType}, {valueTSType}>"; + } + else if (namedType.OriginalDefinition.Name == "IReadOnlyDictionary") + { + string keyTSType = GetTSType(namedType.TypeArguments[0]); + string valueTSType = GetTSType(namedType.TypeArguments[1]); + tsType = $"ReadonlyMap<{keyTSType}, {valueTSType}>"; + } + } + else if (_exportItems.Contains(type, SymbolEqualityComparer.Default)) + { + tsType = type.Name; + } + + if (type.NullableAnnotation == NullableAnnotation.Annotated && + tsType != "any" && !tsType.EndsWith(" | null")) { - // TODO: Get element type. - return "any[]"; + tsType += " | null"; } - return "any"; + return tsType; } - private static string GetTSParameters(IMethodSymbol method, string indent) + private string GetTSParameters(IMethodSymbol method, string indent) { if (method.Parameters.Length == 0) { diff --git a/Runtime/JSArray.cs b/Runtime/JSArray.cs index 0dd9c77a..8744791d 100644 --- a/Runtime/JSArray.cs +++ b/Runtime/JSArray.cs @@ -3,31 +3,34 @@ namespace NodeApi; -public partial struct JSArray : IList +public readonly partial struct JSArray : IList { - private JSValue _value; + private readonly JSValue _value; - public static explicit operator JSArray(JSValue value) => new() { _value = value }; + public static explicit operator JSArray(JSValue value) => new(value); public static implicit operator JSValue(JSArray arr) => arr._value; public static explicit operator JSArray(JSObject obj) => (JSArray)(JSValue)obj; public static implicit operator JSObject(JSArray arr) => (JSObject)arr._value; - public JSArray() + private JSArray(JSValue value) { - _value = JSValue.CreateArray(); + _value = value; } - public JSArray(int length) + public JSArray() : this(JSValue.CreateArray()) + { + } + + public JSArray(int length) : this(JSValue.CreateArray(length)) { - _value = JSValue.CreateArray(length); } public int Length => _value.GetArrayLength(); - public int Count => _value.GetArrayLength(); + int ICollection.Count => _value.GetArrayLength(); - public bool IsReadOnly => false; + bool ICollection.IsReadOnly => false; public JSValue this[int index] { diff --git a/Runtime/JSNativeApi.cs b/Runtime/JSNativeApi.cs index bb802fdf..dcabf467 100644 --- a/Runtime/JSNativeApi.cs +++ b/Runtime/JSNativeApi.cs @@ -563,11 +563,60 @@ public static unsafe Span GetArrayBufferInfo(this JSValue thisValue) public static bool IsTypedArray(this JSValue thisValue) => napi_is_typedarray(Env, (napi_value)thisValue, out c_bool result).ThrowIfFailed((bool)result); - public static unsafe void GetTypedArrayInfo( + public static unsafe int GetTypedArrayLength( + this JSValue thisValue, + out JSTypedArrayType type) + { + napi_get_typedarray_info( + Env, + (napi_value)thisValue, + out napi_typedarray_type type_, + out nuint length_, + out void* _, + out napi_value _, + out nuint _).ThrowIfFailed(); + type = (JSTypedArrayType)(int)type_; + return (int)length_; + } + + public static unsafe Span GetTypedArrayData( + this JSValue thisValue) where T : struct + { + napi_get_typedarray_info( + Env, + (napi_value)thisValue, + out napi_typedarray_type type_, + out nuint length_, + out void* data_, + out napi_value _, + out nuint _).ThrowIfFailed(); + var type = (JSTypedArrayType)(int)type_; + if (!(default(T) switch + { + sbyte => type == JSTypedArrayType.Int8, + byte => type == JSTypedArrayType.UInt8 || type == JSTypedArrayType.UInt8Clamped, + short => type == JSTypedArrayType.Int16, + ushort => type == JSTypedArrayType.Int16, + int => type == JSTypedArrayType.Int32, + uint => type == JSTypedArrayType.UInt32, + long => type == JSTypedArrayType.BigInt64, + ulong => type == JSTypedArrayType.BigUInt64, + float => type == JSTypedArrayType.Float32, + double => type == JSTypedArrayType.Float64, + _ => throw new InvalidCastException("Invalid typed-array type: " + typeof(T)), + })) + { + throw new InvalidCastException( + $"Incorrect typed-array type {typeof(T)} for {type}Array."); + } + + return new Span(data_, (int)length_); + } + + public static unsafe void GetTypedArrayBuffer( this JSValue thisValue, out JSTypedArrayType type, out int length, - out void* data, out JSValue arrayBuffer, out int byteOffset) { @@ -576,7 +625,7 @@ public static unsafe void GetTypedArrayInfo( (napi_value)thisValue, out napi_typedarray_type type_, out nuint length_, - out data, + out void* _, out napi_value arrayBuffer_, out nuint byteOffset_).ThrowIfFailed(); type = (JSTypedArrayType)(int)type_; @@ -793,7 +842,9 @@ internal static unsafe void FinalizeGCHandle(napi_env env, nint data, nint hint) [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] internal static unsafe void FinalizeHintHandle(napi_env _1, nint _2, nint hint) { - GCHandle.FromIntPtr(hint).Free(); + GCHandle handle = GCHandle.FromIntPtr(hint); + (handle.Target as IDisposable)?.Dispose(); + handle.Free(); } [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] @@ -844,15 +895,15 @@ private static unsafe nint[] ToUnmanagedPropertyDescriptors(ReadOnlySpan n private unsafe delegate void UseUnmanagedDescriptors(ReadOnlySpan name, nuint count, napi_property_descriptor* descriptors); - internal class PinnedReadOnlyMemory : IDisposable + internal sealed class PinnedMemory : IDisposable where T : struct { - private bool _disposedValue = false; - private readonly ReadOnlyMemory _memory; + private bool _disposed = false; + private readonly Memory _memory; private MemoryHandle _memoryHandle; public object? Owner { get; private set; } - public PinnedReadOnlyMemory(object? owner, ReadOnlyMemory memory) + public PinnedMemory(Memory memory, object? owner) { Owner = owner; _memory = memory; @@ -861,26 +912,17 @@ public PinnedReadOnlyMemory(object? owner, ReadOnlyMemory memory) public unsafe void* Pointer => _memoryHandle.Pointer; - public int Length => _memory.Length; + public int Length => _memory.Length * Unsafe.SizeOf(); - protected virtual void Dispose(bool disposing) + public void Dispose() { - if (!_disposedValue) + if (!_disposed) { - if (disposing) - { - _memoryHandle.Dispose(); - } - + _disposed = true; + _memoryHandle.Dispose(); Owner = null; - _disposedValue = true; } - } - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); GC.SuppressFinalize(this); } } diff --git a/Runtime/JSObject.cs b/Runtime/JSObject.cs index fec4a322..69c71bc2 100644 --- a/Runtime/JSObject.cs +++ b/Runtime/JSObject.cs @@ -4,20 +4,24 @@ namespace NodeApi; -public partial struct JSObject : IDictionary +public readonly partial struct JSObject : IDictionary { - private JSValue _value; + private readonly JSValue _value; public int Count => throw new System.NotImplementedException(); public bool IsReadOnly => throw new System.NotImplementedException(); - public static explicit operator JSObject(JSValue value) => new() { _value = value }; + public static explicit operator JSObject(JSValue value) => new(value); public static implicit operator JSValue(JSObject obj) => obj._value; - public JSObject() + private JSObject(JSValue value) + { + _value = value; + } + + public JSObject() : this(JSValue.CreateObject()) { - _value = JSValue.CreateObject(); } public void DefineProperties(params JSPropertyDescriptor[] descriptors) diff --git a/Runtime/JSReference.cs b/Runtime/JSReference.cs index f7f5066f..e2de3ab6 100644 --- a/Runtime/JSReference.cs +++ b/Runtime/JSReference.cs @@ -5,8 +5,8 @@ namespace NodeApi; public class JSReference : IDisposable { - private napi_env _env; - private napi_ref _handle; + private readonly napi_env _env; + private readonly napi_ref _handle; public bool IsWeak { get; private set; } diff --git a/Runtime/JSTypedArray.cs b/Runtime/JSTypedArray.cs new file mode 100644 index 00000000..21f97525 --- /dev/null +++ b/Runtime/JSTypedArray.cs @@ -0,0 +1,153 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace NodeApi; + +public readonly struct JSTypedArray where T : struct +{ + private readonly JSValue _value; + + public static explicit operator JSTypedArray(JSValue value) => new(value); + public static implicit operator JSValue(JSTypedArray arr) => arr._value; + + private static int ElementSize { get; } = default(T) switch + { + sbyte => sizeof(sbyte), + byte => sizeof(byte), + short => sizeof(short), + ushort => sizeof(ushort), + int => sizeof(int), + uint => sizeof(uint), + long => sizeof(long), + ulong => sizeof(ulong), + float => sizeof(float), + double => sizeof(double), + _ => throw new InvalidCastException("Invalid typed-array type: " + typeof(T)), + }; + + private static JSTypedArrayType ArrayType { get; } = default(T) switch + { + sbyte => JSTypedArrayType.Int8, + byte => JSTypedArrayType.UInt8, + short => JSTypedArrayType.Int16, + ushort => JSTypedArrayType.UInt16, + int => JSTypedArrayType.Int32, + uint => JSTypedArrayType.UInt32, + long => JSTypedArrayType.BigInt64, + ulong => JSTypedArrayType.BigUInt64, + float => JSTypedArrayType.Float32, + double => JSTypedArrayType.Float64, + _ => throw new InvalidCastException("Invalid typed-array type: " + typeof(T)), + }; + + private JSTypedArray(JSValue value) + { + _value = value; + } + + /// + /// Creates a new typed array of specified length, with newly allocated memroy. + /// + public JSTypedArray(int length) + { + JSValue arrayBuffer = JSValue.CreateArrayBuffer(length * ElementSize); + _value = JSValue.CreateTypedArray(ArrayType, length, arrayBuffer, 0); + } + + /// + /// Creates a typed-array over memory, without copying. + /// + public JSTypedArray(Memory data) + { + JSValue arrayBuffer = JSValue.CreateExternalArrayBuffer(data); + _value = JSValue.CreateTypedArray(ArrayType, data.Length, arrayBuffer, 0); + } + + /// + /// Creates a typed-array over an array, without copying. + /// + public JSTypedArray(T[] data) : this(data.AsMemory()) + { + } + + /// + /// Creates a typed-array over an array, without copying. + /// + public JSTypedArray(T[] data, int start, int length) : this(data.AsMemory().Slice(start, length)) + { + } + + public int Length => _value.GetTypedArrayLength(out _); + + public T this[int index] + { + get => Span[index]; + set => Span[index] = value; + } + + public Span Span => _value.GetTypedArrayData(); + + /// + /// Copies the typed-array data into a new array and returns the array. + /// + public T[] ToArray() => Span.ToArray(); + + /// + /// Copies the typed-array data into an array. + /// + public void CopyTo(T[] array, int arrayIndex) + { + Span.CopyTo(new Span(array, arrayIndex, array.Length - arrayIndex)); + } + + public Span.Enumerator GetEnumerator() => Span.GetEnumerator(); + + /// + /// Gets the typed-array values as memory, without copying. + /// + public Memory AsMemory() + { + JSReference typedArrayReference = new(_value); + return new MemoryManager(typedArrayReference).Memory; + } + + /// + /// Holds a reference to a typed-array value until the memory is disposed. + /// + private class MemoryManager : MemoryManager + { + private readonly JSReference _typedArrayReference; + + public MemoryManager(JSReference typedArrayReference) + { + _typedArrayReference = typedArrayReference; + } + + public override Span GetSpan() + { + JSValue value = _typedArrayReference.GetValue() ?? + throw new ObjectDisposedException(nameof(JSTypedArray)); + return ((JSTypedArray)value).Span; + } + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + // Do TypedArray or ArrayBuffer support pinning? + // This code assumes the memory buffer is not moveable. + void* pointer = Unsafe.AsPointer(ref MemoryMarshal.GetReference(GetSpan())); + return new MemoryHandle(pointer, handle: default, pinnable: this); + } + + public override void Unpin() { } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _typedArrayReference.Dispose(); + } + } + } +} diff --git a/Runtime/JSTypedArrayType.cs b/Runtime/JSTypedArrayType.cs index a67f3ddb..500cc2a1 100644 --- a/Runtime/JSTypedArrayType.cs +++ b/Runtime/JSTypedArrayType.cs @@ -1,17 +1,17 @@ namespace NodeApi; -// Matches to napi_typed_arraytype +// Matches to napi_typedarray_type public enum JSTypedArrayType : int { - SByteArray, - ByteArray, - ByteClampedArray, - Int16Array, - UInt16Array, - Int32Array, - UInt32Array, - SingleArray, - DoubleArray, - BigInt64Array, - BigUInt64Array, + Int8, + UInt8, + UInt8Clamped, + Int16, + UInt16, + Int32, + UInt32, + Float32, + Float64, + BigInt64, + BigUInt64, } diff --git a/Runtime/JSValue.cs b/Runtime/JSValue.cs index cc95cb81..f014cdc5 100644 --- a/Runtime/JSValue.cs +++ b/Runtime/JSValue.cs @@ -6,9 +6,9 @@ namespace NodeApi; -public struct JSValue +public readonly struct JSValue { - private napi_value _handle; + private readonly napi_value _handle; public JSValueScope Scope { get; } @@ -188,9 +188,9 @@ public static unsafe JSValue CreateArrayBuffer(ReadOnlySpan data) return result; } - public static unsafe JSValue CreateExternalArrayBuffer(object? external, ReadOnlyMemory memory) + public static unsafe JSValue CreateExternalArrayBuffer(Memory memory, object? external = null) where T : struct { - var pinnedMemory = new PinnedReadOnlyMemory(external, memory); + var pinnedMemory = new PinnedMemory(memory, external); return napi_create_external_arraybuffer( Env, pinnedMemory.Pointer, diff --git a/Runtime/JSValueScope.cs b/Runtime/JSValueScope.cs index 18285154..c216509c 100644 --- a/Runtime/JSValueScope.cs +++ b/Runtime/JSValueScope.cs @@ -5,7 +5,7 @@ namespace NodeApi; public class JSValueScope : IDisposable { - private napi_env _env; + private readonly napi_env _env; [ThreadStatic] private static JSValueScope? s_current; public JSValueScope? ParentScope { get; } diff --git a/Test/TestCases/napi-dotnet/ComplexTypes.cs b/Test/TestCases/napi-dotnet/ComplexTypes.cs index 4ed742b0..f5d65529 100644 --- a/Test/TestCases/napi-dotnet/ComplexTypes.cs +++ b/Test/TestCases/napi-dotnet/ComplexTypes.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Threading; namespace NodeApi.TestCases; @@ -23,9 +21,9 @@ public static class ComplexTypes public static ClassObject? NullableClassObject { get; set; } - public static int[] Array { get; set; } = System.Array.Empty(); + public static string[] StringArray { get; set; } = Array.Empty(); - public static int[]? NullableArray { get; set; } + public static Memory UIntArray { get; set; } public static IList List { get; set; } = new List(); @@ -35,6 +33,9 @@ public static class ComplexTypes public static IReadOnlyDictionary ReadOnlyDictionary { get; set; } = new Dictionary().AsReadOnly(); + + public static IDictionary> ObjectListDictionary { get; set; } + = new Dictionary>(); } /// diff --git a/Test/TestCases/napi-dotnet/complex_types.js b/Test/TestCases/napi-dotnet/complex_types.js index 0d7c4921..bd6f5809 100644 --- a/Test/TestCases/napi-dotnet/complex_types.js +++ b/Test/TestCases/napi-dotnet/complex_types.js @@ -57,3 +57,35 @@ assert.notStrictEqual(structInstance2, structInstance); structInstance2.value = 'test2'; assert.strictEqual(structInstance2.value, 'test2'); assert.strictEqual(structInstance.value, 'test'); + +// C# arrays are copied to/from JS, so modifying the returned array doesn't affect the original. +const stringArrayValue = ComplexTypes.stringArray; +assert(Array.isArray(stringArrayValue)); +assert.strictEqual(stringArrayValue.length, 0); +ComplexTypes.stringArray = [ 'test' ]; +assert.notStrictEqual(ComplexTypes.stringArray, stringArrayValue); +assert.strictEqual(ComplexTypes.stringArray[0], 'test'); +ComplexTypes.stringArray[0] = 'test2'; +assert.strictEqual(ComplexTypes.stringArray[0], 'test'); + +// C# Memory maps to/from JS TypedArray (without copying) for valid typed-array element types. +const uintArrayValue = ComplexTypes.uIntArray; +assert(uintArrayValue instanceof Uint32Array); +assert.strictEqual(uintArrayValue.length, 0); +const uintArrayValue2 = new Uint32Array([0, 1, 2]); +ComplexTypes.uIntArray = uintArrayValue2; +assert.strictEqual(ComplexTypes.uIntArray.length, 3); +assert.strictEqual(ComplexTypes.uIntArray[1], 1); + +/* +// C# IList maps to/from JS Array (without copying). +const listValue = ComplexTypes.list; +assert(Array.isArray(listValue)); +assert.strictEqual(listValue.length, 0); +ComplexTypes.list = [0]; +assert.notStrictEqual(ComplexTypes.list, listValue); +assert.strictEqual(ComplexTypes.list[0], 0); +listValue = ComplexTypes.list; +ComplexTypes.list[0] = 1; +assert.strictEqual(listValue[0], 1); +*/