From b4601f0344f3a50f876e574a196f816ab6fab326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ansis=20M=C4=81li=C5=86=C5=A1?= Date: Fri, 17 Oct 2025 09:46:52 +0200 Subject: [PATCH] Non-allocating JavaScript object access --- ClearScript/V8/FastProxy/V8FastArgs.cs | 11 ++ .../V8/SplitProxy/IV8SplitProxyNative.cs | 1 + .../V8/SplitProxy/V8SplitProxyHelpers.cs | 131 +++++++++++++----- .../SplitProxy/V8SplitProxyNative.Common.tt | 8 ++ .../V8SplitProxyNative.Generated.cs | 69 +++++++++ 5 files changed, 188 insertions(+), 32 deletions(-) diff --git a/ClearScript/V8/FastProxy/V8FastArgs.cs b/ClearScript/V8/FastProxy/V8FastArgs.cs index 0f500439..fa1028a2 100644 --- a/ClearScript/V8/FastProxy/V8FastArgs.cs +++ b/ClearScript/V8/FastProxy/V8FastArgs.cs @@ -432,6 +432,17 @@ internal V8FastArgs(V8ScriptEngine engine, in ReadOnlySpan args /// public T Get(int index, string name = null) => V8FastArgImpl.Get(args[index], GetObject(index), argKind, name); + /// + /// + /// + /// + /// + /// + public V8Object GetV8Object(int index, string name = null) + { + return new V8Object((V8Object.Handle)args[index].data.PtrOrHandle, args[index].data.IdentityHash); + } + private static void EnsureObjects(ref object[] objects, int count) { if (objects is null) diff --git a/ClearScript/V8/SplitProxy/IV8SplitProxyNative.cs b/ClearScript/V8/SplitProxy/IV8SplitProxyNative.cs index 165dd25e..751f8aca 100644 --- a/ClearScript/V8/SplitProxy/IV8SplitProxyNative.cs +++ b/ClearScript/V8/SplitProxy/IV8SplitProxyNative.cs @@ -208,6 +208,7 @@ internal interface IV8SplitProxyNative #region V8 object methods object V8Object_GetNamedProperty(V8Object.Handle hObject, string name); + void V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue); bool V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value); void V8Object_SetNamedProperty(V8Object.Handle hObject, string name, object value); bool V8Object_DeleteNamedProperty(V8Object.Handle hObject, string name); diff --git a/ClearScript/V8/SplitProxy/V8SplitProxyHelpers.cs b/ClearScript/V8/SplitProxy/V8SplitProxyHelpers.cs index a5e31923..81877d95 100644 --- a/ClearScript/V8/SplitProxy/V8SplitProxyHelpers.cs +++ b/ClearScript/V8/SplitProxy/V8SplitProxyHelpers.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using Microsoft.ClearScript.Util; +using Microsoft.ClearScript.V8.FastProxy; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -9,8 +12,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Microsoft.ClearScript.Util; -using Microsoft.ClearScript.V8.FastProxy; +using System.Xml.Linq; namespace Microsoft.ClearScript.V8.SplitProxy { @@ -876,14 +878,17 @@ public readonly struct Ptr #endregion } - internal static class V8Value + /// + /// + /// + public static class V8Value { - public const int Size = 16; + internal const int Size = 16; private const short positiveOrZero = 0; private const short negative = 1; - public static ValueScope CreateScope() + internal static ValueScope CreateScope() { return ScopeFactory.Create( static () => V8SplitProxyNative.InvokeRaw(static instance => instance.V8Value_New()), @@ -891,14 +896,14 @@ public static ValueScope CreateScope() ); } - public static ValueScope CreateScope(object obj) + internal static ValueScope CreateScope(object obj) { var scope = CreateScope(); Set(scope.Value, obj); return scope; } - public static void Set(Ptr pV8Value, object obj) + internal static void Set(Ptr pV8Value, object obj) { if (obj is Nonexistent) { @@ -990,7 +995,7 @@ public static void Set(Ptr pV8Value, object obj) } } - public static object Get(Ptr pV8Value) + internal static object Get(Ptr pV8Value) { return V8SplitProxyNative.InvokeRaw( static (instance, pV8Value) => @@ -1074,7 +1079,7 @@ private static void SetHostObject(Ptr pV8Value, IHostItem hostObject) #region Nested type: Type - public enum Type : byte + internal enum Type : byte { // IMPORTANT: maintain bitwise equivalence with native enum V8Value::Type Nonexistent, @@ -1093,7 +1098,7 @@ public enum Type : byte #region Nested type: Subtype - public enum Subtype : byte + internal enum Subtype : byte { // IMPORTANT: maintain bitwise equivalence with native enum V8Value::Subtype None, @@ -1121,7 +1126,7 @@ public enum Subtype : byte #region Nested type: Flags [Flags] - public enum Flags : ushort + internal enum Flags : ushort { // IMPORTANT: maintain bitwise equivalence with native enum V8Value::Flags None = 0, @@ -1133,14 +1138,14 @@ public enum Flags : ushort Rejected = 0x0010 } - public static bool HasAllFlags(this Flags value, Flags flags) => (value & flags) == flags; - public static bool HasAnyFlag(this Flags value, Flags flags) => (value & flags) != 0; + internal static bool HasAllFlags(this Flags value, Flags flags) => (value & flags) == flags; + internal static bool HasAnyFlag(this Flags value, Flags flags) => (value & flags) != 0; #endregion #region Nested type: Ptr - public readonly struct Ptr + internal readonly struct Ptr { private readonly IntPtr bits; @@ -1167,7 +1172,7 @@ public readonly struct Ptr #region Nested type: WireData [StructLayout(LayoutKind.Explicit)] - public struct WireData + internal struct WireData { // IMPORTANT: maintain bitwise equivalence with native struct V8Value::WireData [FieldOffset(0)] public Type Type; @@ -1213,10 +1218,10 @@ public readonly bool TryCreateBigInteger(out BigInteger value) #region Nested type: Decoded [StructLayout(LayoutKind.Explicit)] - public struct Decoded + internal struct Decoded { // IMPORTANT: maintain bitwise equivalence with native struct V8Value::Decoded - [FieldOffset(0)] private WireData data; + [FieldOffset(0)] internal WireData data; public object Get() { @@ -1303,17 +1308,25 @@ public readonly struct Ptr #region Nested type: FastArg + /// + /// + /// [StructLayout(LayoutKind.Explicit)] public readonly struct FastArg { + internal FastArg(WireData data) + { + this.data = data; + } + // IMPORTANT: maintain bitwise equivalence with native struct V8Value::FastArg - [FieldOffset(0)] private readonly WireData data; + [FieldOffset(0)] internal readonly WireData data; - public bool IsUndefined() => (data.Type == Type.Undefined) || (data.Type == Type.Nonexistent); + internal bool IsUndefined() => (data.Type == Type.Undefined) || (data.Type == Type.Nonexistent); - public bool IsNull() => data.Type == Type.Null; + internal bool IsNull() => data.Type == Type.Null; - public bool TryGetBoolean(out bool value) + internal bool TryGetBoolean(out bool value) { if (data.Type == Type.Boolean) { @@ -1325,7 +1338,7 @@ public bool TryGetBoolean(out bool value) return false; } - public bool TryGetNumber(out double value) + internal bool TryGetNumber(out double value) { if (data.Type == Type.Number) { @@ -1337,7 +1350,7 @@ public bool TryGetNumber(out double value) return false; } - public bool TryGetString(out string value) + internal bool TryGetString(out string value) { if (data.Type == Type.String) { @@ -1349,7 +1362,7 @@ public bool TryGetString(out string value) return false; } - public unsafe bool TryGetCharSpan(out ReadOnlySpan value) + internal unsafe bool TryGetCharSpan(out ReadOnlySpan value) { if (data.Type == Type.String) { @@ -1361,7 +1374,7 @@ public unsafe bool TryGetCharSpan(out ReadOnlySpan value) return false; } - public bool TryGetDateTime(out DateTime value) + internal bool TryGetDateTime(out DateTime value) { if (data.Type == Type.DateTime) { @@ -1373,7 +1386,7 @@ public bool TryGetDateTime(out DateTime value) return false; } - public bool TryGetBigInteger(out BigInteger value) + internal bool TryGetBigInteger(out BigInteger value) { if (data.Type == Type.BigInt) { @@ -1384,7 +1397,7 @@ public bool TryGetBigInteger(out BigInteger value) return false; } - public bool TryGetV8Object(out V8ObjectImpl v8Object) + internal bool TryGetV8Object(out V8ObjectImpl v8Object) { if (data.Type == Type.V8Object) { @@ -1396,7 +1409,7 @@ public bool TryGetV8Object(out V8ObjectImpl v8Object) return false; } - public bool TryGetHostObject(out object hostObject) + internal bool TryGetHostObject(out object hostObject) { if (data.Type == Type.HostObject) { @@ -1411,7 +1424,7 @@ public bool TryGetHostObject(out object hostObject) #region Nested type: Ptr // ReSharper disable once MemberHidesStaticFromOuterClass - public readonly struct Ptr + internal readonly struct Ptr { private readonly IntPtr bits; @@ -1444,7 +1457,7 @@ public readonly struct Ptr #region Nested type: FastResult [StructLayout(LayoutKind.Explicit)] - public struct FastResult + internal struct FastResult { // IMPORTANT: maintain bitwise equivalence with native struct V8Value::FastResult [FieldOffset(0)] private WireData data; @@ -2000,11 +2013,65 @@ public readonly struct Handle #endregion } - internal static class V8Object + /// + /// + /// + public readonly ref struct V8Object { + internal readonly Handle handle; + private readonly int identityHash; + + internal V8Object(Handle hObject, int identityHash) + { + if (hObject == Handle.Empty) + throw new ArgumentNullException(nameof(hObject)); + + handle = hObject; + this.identityHash = identityHash; + } + + /// + /// Return the identity hash of the JavaScript object. + /// + /// The identity hash of the JavaScript object. + public override int GetHashCode() + { + return identityHash; + } + + /// + /// Obtain the value of a named property of the wrapped JavaScript object. + /// + /// The name of the property. + /// + /// + public unsafe V8FastArg GetProperty(string name, ref V8Value.FastArg storage) + { + if (handle == Handle.Empty) + throw new NullReferenceException("V8 object is uninitialized"); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + V8Value.Decoded decoded = V8SplitProxyNative.InvokeRaw((instance, arg) => + { + V8Value.Ptr pValue = instance.V8Value_New(); + instance.V8Object_GetNamedProperty(arg.handle, arg.name, pValue); + instance.V8Value_Decode(pValue, out var decoded); + instance.V8Value_Delete(pValue); + return decoded; + }, (handle, name)); + + storage = new V8Value.FastArg(decoded.data); + + return new V8FastArg((V8ScriptEngine)ScriptEngine.Current, + (V8Value.FastArg.Ptr)(IntPtr)Unsafe.AsPointer(ref storage), + V8FastArgKind.PropertyValue); + } + #region Nested type: Handle - public readonly struct Handle + internal readonly struct Handle { private readonly IntPtr guts; diff --git a/ClearScript/V8/SplitProxy/V8SplitProxyNative.Common.tt b/ClearScript/V8/SplitProxy/V8SplitProxyNative.Common.tt index 0e914d70..6c9625e8 100644 --- a/ClearScript/V8/SplitProxy/V8SplitProxyNative.Common.tt +++ b/ClearScript/V8/SplitProxy/V8SplitProxyNative.Common.tt @@ -965,6 +965,14 @@ namespace Microsoft.ClearScript.V8.SplitProxy } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) diff --git a/ClearScript/V8/SplitProxy/V8SplitProxyNative.Generated.cs b/ClearScript/V8/SplitProxy/V8SplitProxyNative.Generated.cs index 5d542578..2a310374 100644 --- a/ClearScript/V8/SplitProxy/V8SplitProxyNative.Generated.cs +++ b/ClearScript/V8/SplitProxy/V8SplitProxyNative.Generated.cs @@ -4,6 +4,11 @@ + + + + + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. @@ -1007,6 +1012,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -3204,6 +3217,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -5401,6 +5422,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -7598,6 +7627,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -9795,6 +9832,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -11992,6 +12037,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -14189,6 +14242,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name)) @@ -16386,6 +16447,14 @@ object IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, st } } + void IV8SplitProxyNative.V8Object_GetNamedProperty(V8Object.Handle hObject, string name, V8Value.Ptr pValue) + { + using (var nameScope = StdString.CreateScope(name)) + { + V8Object_GetNamedProperty(hObject, nameScope.Value, pValue); + } + } + bool IV8SplitProxyNative.V8Object_TryGetNamedProperty(V8Object.Handle hObject, string name, out object value) { using (var nameScope = StdString.CreateScope(name))