Skip to content

Commit b06576a

Browse files
authored
Merge pull request #22 from 6over3/sg-fix
fix(cs): various source generation / perf issues
2 parents 751abec + 227eb81 commit b06576a

14 files changed

Lines changed: 254 additions & 74 deletions

hosts/dotnet/Hako.SourceGenerator.Tests/JSBindingGeneratorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public class Realm
225225
public class HakoRuntime
226226
{
227227
public void RegisterJSClass<T>(JSClass jsClass) { }
228-
public JSClass GetJSClass<T>() { return null; }
228+
public JSClass GetJSClass<T>(Realm realm) { return null; }
229229
public Realm GetSystemRealm() { return new Realm(); }
230230
public HakoJS.Host.CModule CreateCModule(string name, System.Action<object> init, Realm realm) { return new HakoJS.Host.CModule(); }
231231
}

hosts/dotnet/Hako.SourceGenerator/JSBindingGenerator.Bindings.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ private static void GenerateMarshalingMethods(StringBuilder sb, ClassModel model
444444

445445
sb.AppendLine(" public global::HakoJS.VM.JSValue ToJSValue(global::HakoJS.VM.Realm realm)");
446446
sb.AppendLine(" {");
447-
sb.AppendLine($" var jsClass = realm.Runtime.GetJSClass<{model.ClassName}>();");
447+
sb.AppendLine($" var jsClass = realm.Runtime.GetJSClass<{model.ClassName}>(realm);");
448448
sb.AppendLine(" if (jsClass == null)");
449449
sb.AppendLine(" {");
450450
sb.AppendLine(" throw new global::System.InvalidOperationException(");
@@ -482,7 +482,12 @@ private static void GenerateMarshalingMethods(StringBuilder sb, ClassModel model
482482

483483
private static bool IsArrayType(TypeInfo type)
484484
{
485-
return type.IsArray && type.FullName != "global::System.Byte[]";
485+
// Exclude byte[] from generic array handling (it uses ArrayBuffer/TypedArray)
486+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
487+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
488+
return false;
489+
490+
return type.IsArray;
486491
}
487492

488493
#endregion

hosts/dotnet/Hako.SourceGenerator/JSBindingGenerator.Marshaling.cs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,8 @@ private static string GetTypeCheck(TypeInfo type, string jsValueName)
10451045
? $"{jsValueName}.IsNumber()"
10461046
: $"{jsValueName}.IsString()";
10471047

1048-
if (type.FullName == "global::System.Byte[]")
1048+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
1049+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
10491050
return $"({jsValueName}.IsArrayBuffer() || {jsValueName}.IsTypedArray())";
10501051

10511052
if (type.IsArray)
@@ -1157,7 +1158,8 @@ private static string GetStrictUnmarshalCode(TypeInfo type, string jsValueName,
11571158
return $"global::System.Enum.Parse<{type.FullName}>({jsValueName}.AsString(), ignoreCase: true)";
11581159
}
11591160

1160-
if (type.FullName == "global::System.Byte[]")
1161+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
1162+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
11611163
return
11621164
$"({jsValueName}.IsArrayBuffer() ? {jsValueName}.CopyArrayBuffer() : {jsValueName}.CopyTypedArray())";
11631165

@@ -1196,6 +1198,58 @@ private static string GetUnmarshalCode(TypeInfo type, string jsValueName, string
11961198
if (type.SpecialType == SpecialType.System_Object)
11971199
return $"{jsValueName}.GetNativeValue<object>()";
11981200

1201+
// Handle generic dictionaries
1202+
if (type is { IsGenericDictionary: true, KeyTypeSymbol: not null, ValueTypeSymbol: not null })
1203+
{
1204+
var keyTypeInfo = CreateTypeInfo(type.KeyTypeSymbol);
1205+
var isKeyValid = keyTypeInfo.SpecialType == SpecialType.System_String ||
1206+
IsNumericType(type.KeyTypeSymbol);
1207+
1208+
if (isKeyValid)
1209+
{
1210+
var isValueMarshalable = ImplementsIJSMarshalable(type.ValueTypeSymbol) ||
1211+
HasAttribute(type.ValueTypeSymbol,
1212+
"HakoJS.SourceGeneration.JSClassAttribute") ||
1213+
HasAttribute(type.ValueTypeSymbol,
1214+
"HakoJS.SourceGeneration.JSObjectAttribute");
1215+
1216+
if (isValueMarshalable)
1217+
return $"{jsValueName}.ToDictionaryOf<{type.KeyType}, {type.ValueType}>()";
1218+
1219+
return $"{jsValueName}.ToDictionary<{type.KeyType}, {type.ValueType}>()";
1220+
}
1221+
1222+
return $"{jsValueName}.GetNativeValue<object>()";
1223+
}
1224+
1225+
// Handle generic collections (List<T>, IList<T>, etc.)
1226+
if (type is { IsGenericCollection: true, ItemTypeSymbol: not null })
1227+
{
1228+
var isItemMarshalable = ImplementsIJSMarshalable(type.ItemTypeSymbol) ||
1229+
HasAttribute(type.ItemTypeSymbol, "HakoJS.SourceGeneration.JSClassAttribute") ||
1230+
HasAttribute(type.ItemTypeSymbol, "HakoJS.SourceGeneration.JSObjectAttribute");
1231+
1232+
var arrayMethod = isItemMarshalable ? "ToArrayOf" : "ToArray";
1233+
var arrayExpr = $"{jsValueName}.{arrayMethod}<{type.ItemType}>()";
1234+
1235+
// Check the specific collection type and convert appropriately
1236+
var typeDefinition = type.FullName.Replace("global::", "");
1237+
if (typeDefinition.StartsWith("System.Collections.Generic.List<"))
1238+
// For List<T>, wrap the array in a List constructor
1239+
return $"new {type.FullName}({arrayExpr})";
1240+
1241+
if (typeDefinition.StartsWith("System.Collections.Generic.IList<") ||
1242+
typeDefinition.StartsWith("System.Collections.Generic.ICollection<") ||
1243+
typeDefinition.StartsWith("System.Collections.Generic.IEnumerable<") ||
1244+
typeDefinition.StartsWith("System.Collections.Generic.IReadOnlyList<") ||
1245+
typeDefinition.StartsWith("System.Collections.Generic.IReadOnlyCollection<"))
1246+
// For interfaces, the array can be used directly (implicit conversion)
1247+
return arrayExpr;
1248+
1249+
// Fallback for other collection types
1250+
return arrayExpr;
1251+
}
1252+
11991253
switch (type.SpecialType)
12001254
{
12011255
case SpecialType.System_String:
@@ -1226,12 +1280,15 @@ private static string GetUnmarshalCode(TypeInfo type, string jsValueName, string
12261280
return $"{jsValueName}.AsDateTime()";
12271281
}
12281282

1229-
if (type.FullName == "global::System.Byte[]")
1283+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
1284+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
12301285
return
12311286
$"({jsValueName}.IsArrayBuffer() ? {jsValueName}.CopyArrayBuffer() : {jsValueName}.CopyTypedArray())";
12321287

12331288
if (type is { IsArray: true, ElementType: not null })
12341289
{
1290+
var elementTypeName = type.ElementType.Replace("global::", "");
1291+
12351292
// Check if element is a [JSEnum]
12361293
if (type.ItemTypeSymbol != null && type.ItemTypeSymbol.IsJSEnum())
12371294
{
@@ -1247,6 +1304,14 @@ private static string GetUnmarshalCode(TypeInfo type, string jsValueName, string
12471304
$"{jsValueName}.ToArray<string>().Select(x => global::System.Enum.Parse<{fullEnumType}>(x, ignoreCase: true)).ToArray()";
12481305
}
12491306
}
1307+
1308+
// Handle arrays of primitives
1309+
if (IsPrimitiveTypeName(type.ElementType) ||
1310+
elementTypeName is "System.Object" or "object")
1311+
return $"{jsValueName}.ToArray<{type.ElementType}>()";
1312+
1313+
// Handle arrays of marshalable types
1314+
return $"{jsValueName}.ToArrayOf<{type.ElementType}>()";
12501315
}
12511316

12521317
return $"{type.FullName}.FromJSValue({contextVarName}, {jsValueName})";
@@ -1298,7 +1363,8 @@ private static string GetTypeName(TypeInfo type)
12981363
// Handle [JSEnum]
12991364
if (type.IsEnum) return type.IsFlags ? "a number (flags enum)" : "a string (enum)";
13001365

1301-
if (type.FullName == "global::System.Byte[]")
1366+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
1367+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
13021368
return "an ArrayBuffer or TypedArray";
13031369

13041370
if (type.IsArray)
@@ -1422,7 +1488,8 @@ private static string GetMarshalCodeForPrimitive(TypeInfo type, string valueName
14221488
: $"{ctxName}.NewString({valueName}.ToStringFast())";
14231489
}
14241490

1425-
if (type.FullName == "global::System.Byte[]")
1491+
if (type.FullName is "global::System.Byte[]" or "byte[]" ||
1492+
(type.IsArray && type.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
14261493
return type.IsNullable
14271494
? $"({valueName} == null ? {ctxName}.Null() : {ctxName}.NewArrayBuffer({valueName}))"
14281495
: $"{ctxName}.NewArrayBuffer({valueName})";

hosts/dotnet/Hako.SourceGenerator/JSBindingGenerator.TypeScript.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,8 @@ private static void AddTypeDependency(HashSet<string> dependencies, TypeInfo typ
925925
if (typeInfo.SpecialType != SpecialType.None)
926926
return;
927927

928-
if (typeInfo.FullName == "global::System.Byte[]")
928+
if (typeInfo.FullName is "global::System.Byte[]" or "byte[]" ||
929+
(typeInfo.IsArray && typeInfo.ItemTypeSymbol?.SpecialType == SpecialType.System_Byte))
929930
return;
930931

931932
if (IsSpecialMarshalingType(typeInfo.FullName))

hosts/dotnet/Hako/Builders/JSObjectBuilder.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -451,19 +451,21 @@ private void DefineProperty(JSValue obj, string key, JSValue value, IPropertyEnt
451451
{
452452
using var keyValue = _context.NewString(key);
453453

454+
var flags = PropFlags.HasWritable;
455+
if (entry.Configurable) flags |= PropFlags.Configurable;
456+
if (entry.Enumerable) flags |= PropFlags.Enumerable;
457+
if (entry.Writable) flags |= PropFlags.Writable;
458+
459+
using var desc = _context.Runtime.Memory.AllocateDataPropertyDescriptor(
460+
_context.Pointer,
461+
value.GetHandle(),
462+
flags);
463+
454464
var result = _context.Runtime.Registry.DefineProp(
455465
_context.Pointer,
456-
obj.GetHandle(), // this_obj
457-
keyValue.GetHandle(), // prop_name (string)
458-
value.GetHandle(), // prop_value (data)
459-
_context.Runtime.Registry.GetUndefined(), // get
460-
_context.Runtime.Registry.GetUndefined(), // set
461-
entry.Configurable ? 1 : 0, // configurable (presence+value handled in native)
462-
entry.Enumerable ? 1 : 0, // enumerable
463-
1, // hasValue (we always pass a value here)
464-
1, // hasWritable (we always specify writability)
465-
entry.Writable ? 1 : 0 // writable
466-
);
466+
obj.GetHandle(),
467+
keyValue.GetHandle(),
468+
desc.Value);
467469

468470
if (result == -1)
469471
{

hosts/dotnet/Hako/Extensions/HakoRuntimeExtensions.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,31 +51,35 @@ public static ModuleLoaderBuilder ConfigureModules(this HakoRuntime runtime)
5151

5252

5353
/// <summary>
54-
/// Registers a JSClass in the runtime's global registry.
54+
/// Registers a JSClass in the runtime's registry for the specified realm.
5555
/// This allows bidirectional marshaling between C# and JavaScript.
56-
/// Classes are automatically cleaned up when their associated context is disposed.
56+
/// Classes are automatically cleaned up when their associated realm is disposed.
5757
/// </summary>
5858
public static void RegisterJSClass<T>(this HakoRuntime runtime, JSClass jsClass) where T : class, IJSBindable<T>
5959
{
6060
ArgumentNullException.ThrowIfNull(runtime);
6161
ArgumentNullException.ThrowIfNull(T.TypeKey);
6262
ArgumentNullException.ThrowIfNull(jsClass);
63-
if (!runtime.JSClassRegistry.TryAdd(T.TypeKey, jsClass))
63+
64+
var key = (jsClass.Context.Pointer, T.TypeKey);
65+
if (!runtime.JSClassRegistry.TryAdd(key, jsClass))
6466
{
65-
throw new HakoException($"Failed to register JSClass for type '{T.TypeKey}'");
67+
throw new HakoException($"JSClass for type '{T.TypeKey}' is already registered in this realm");
6668
}
6769
}
6870

6971
/// <summary>
70-
/// Gets a previously registered JSClass by its type key.
72+
/// Gets a previously registered JSClass by its type key for the specified realm.
7173
/// Returns null if the class hasn't been registered.
7274
/// </summary>
73-
public static JSClass? GetJSClass<T>(this HakoRuntime runtime) where T : class, IJSBindable<T>
75+
public static JSClass? GetJSClass<T>(this HakoRuntime runtime, Realm realm) where T : class, IJSBindable<T>
7476
{
7577
ArgumentNullException.ThrowIfNull(runtime);
78+
ArgumentNullException.ThrowIfNull(realm);
7679
ArgumentNullException.ThrowIfNull(T.TypeKey);
7780

78-
return runtime.JSClassRegistry.GetValueOrDefault(T.TypeKey);
81+
var key = (realm.Pointer, T.TypeKey);
82+
return runtime.JSClassRegistry.GetValueOrDefault(key);
7983
}
8084

8185
public static CModule CreateModule<T>(

hosts/dotnet/Hako/Extensions/JSValueExtensions.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Runtime.CompilerServices;
22
using HakoJS.Exceptions;
3+
using HakoJS.Host;
34
using HakoJS.Lifetime;
45
using HakoJS.SourceGeneration;
56
using HakoJS.VM;
@@ -969,26 +970,28 @@ private static void DefineProperty(
969970
var realm = obj.Realm;
970971
using var keyValue = realm.NewString(key);
971972

973+
var flags = PropFlags.HasWritable;
974+
if (configurable) flags |= PropFlags.Configurable;
975+
if (enumerable) flags |= PropFlags.Enumerable;
976+
if (writable) flags |= PropFlags.Writable;
977+
978+
using var desc = realm.Runtime.Memory.AllocateDataPropertyDescriptor(
979+
realm.Pointer,
980+
value.GetHandle(),
981+
flags);
982+
972983
var result = realm.Runtime.Registry.DefineProp(
973984
realm.Pointer,
974985
obj.GetHandle(),
975986
keyValue.GetHandle(),
976-
value.GetHandle(),
977-
realm.Runtime.Registry.GetUndefined(), // get
978-
realm.Runtime.Registry.GetUndefined(), // set
979-
configurable ? 1 : 0,
980-
enumerable ? 1 : 0,
981-
1, // hasValue
982-
1, // hasWritable
983-
writable ? 1 : 0
984-
);
987+
desc.Value);
985988

986989
if (result == -1)
987990
{
988991
var exception = realm.GetLastError();
989992
if (exception is not null)
990993
throw new HakoException($"Failed to define property '{key}'", exception);
991-
994+
992995
throw new HakoException($"Failed to define property '{key}'",
993996
new JavaScriptException("(unknown error)"));
994997
}

hosts/dotnet/Hako/Host/HakoRegistry.cs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//------------------------------------------------------------------------------
22
// <auto-generated>
33
// This code was generated by a tool.
4-
// Hako Version: v1.0.5-3-gfc65b2d-dirty
4+
// Hako Version: v1.0.13-5-g23bf654-dirty
55
//
66
// Changes to this file may cause incorrect behavior and will be lost if
77
// the code is regenerated.
@@ -79,7 +79,7 @@ private void InitializeFunctions()
7979
_getProp = TryCreateFuncInt32<int, int, int>("HAKO_GetProp");
8080
_getPropNumber = TryCreateFuncInt32<int, int, int>("HAKO_GetPropNumber");
8181
_setProp = TryCreateFuncInt32<int, int, int, int>("HAKO_SetProp");
82-
_defineProp = TryCreateFuncInt32<int, int, int, int, int, int, int, int, int, int, int>("HAKO_DefineProp");
82+
_defineProp = TryCreateFuncInt32<int, int, int, int>("HAKO_DefineProp");
8383
_getOwnPropertyNames = TryCreateFuncInt32<int, int, int, int, int>("HAKO_GetOwnPropertyNames");
8484
_call = TryCreateFuncInt32<int, int, int, int, int>("HAKO_Call");
8585
_getLastError = TryCreateFuncInt32<int, int>("HAKO_GetLastError");
@@ -202,7 +202,7 @@ private void InitializeFunctions()
202202
private Func<int, int, int, int>? _getProp;
203203
private Func<int, int, int, int>? _getPropNumber;
204204
private Func<int, int, int, int, int>? _setProp;
205-
private Func<int, int, int, int, int, int, int, int, int, int, int, int>? _defineProp;
205+
private Func<int, int, int, int, int>? _defineProp;
206206
private Func<int, int, int, int, int, int>? _getOwnPropertyNames;
207207
private Func<int, int, int, int, int, int>? _call;
208208
private Func<int, int, int>? _getLastError;
@@ -287,11 +287,6 @@ private void InitializeFunctions()
287287
return _instance.GetFunctionInt32(functionName);
288288
}
289289

290-
private Func<int, int, int, int, int, int, int, int, int, int, int, int>? TryCreateFuncInt32<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(string functionName)
291-
{
292-
return _instance.GetFunctionInt32<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(functionName);
293-
}
294-
295290
private Func<int, int>? TryCreateFuncInt32<T1>(string functionName)
296291
{
297292
return _instance.GetFunctionInt32<T1>(functionName);
@@ -1040,22 +1035,15 @@ public int SetProp(JSContextPointer ctx, JSValuePointer this_val, JSValuePointer
10401035
/// <param name="ctx">Context to use</param>
10411036
/// <param name="this_val">Object to define property on</param>
10421037
/// <param name="prop_name">Property name value</param>
1043-
/// <param name="prop_val">Property value (if has_value is true)</param>
1044-
/// <param name="getter">Getter function or undefined</param>
1045-
/// <param name="setter">Setter function or undefined</param>
1046-
/// <param name="configurable">Property is configurable</param>
1047-
/// <param name="enumerable">Property is enumerable</param>
1048-
/// <param name="has_value">Descriptor includes value</param>
1049-
/// <param name="has_writable">Descriptor includes writable</param>
1050-
/// <param name="writable">Property is writable (if has_writable)</param>
1038+
/// <param name="desc">Property descriptor with value/accessors and flags</param>
10511039
/// <returns>1 on success, 0 on failure, -1 on exception</returns>
1052-
public int DefineProp(JSContextPointer ctx, JSValuePointer this_val, JSValuePointer prop_name, JSValuePointer prop_val, JSValuePointer getter, JSValuePointer setter, int configurable, int enumerable, int has_value, int has_writable, int writable)
1040+
public int DefineProp(JSContextPointer ctx, JSValuePointer this_val, JSValuePointer prop_name, PropDescriptorPointer desc)
10531041
{
10541042
return Hako.Dispatcher.Invoke(() =>
10551043
{
10561044
if (_defineProp == null)
10571045
throw new InvalidOperationException("HAKO_DefineProp not available");
1058-
return _defineProp(ctx, this_val, prop_name, prop_val, getter, setter, configurable, enumerable, has_value, has_writable, writable);
1046+
return _defineProp(ctx, this_val, prop_name, desc);
10591047
});
10601048
}
10611049

hosts/dotnet/Hako/Host/HakoRuntime.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public sealed class HakoRuntime : IDisposable
4545
private PromiseRejectionTrackerFunction? _currentPromiseRejectionTracker;
4646
private bool _disposed;
4747
private Realm? _systemRealm;
48-
internal readonly ConcurrentDictionary<string, JSClass> JSClassRegistry = new();
48+
internal readonly ConcurrentDictionary<(int RealmPtr, string TypeKey), JSClass> JSClassRegistry = new();
4949
private readonly WasmEngine _engine;
5050
private readonly WasmStore _store;
5151
private readonly WasmInstance _instance;
@@ -368,21 +368,21 @@ internal void DisposeJSClassesForRealm(int realmPtr)
368368
{
369369
// Find and dispose all JSClasses belonging to this realm
370370
var classesToRemove = JSClassRegistry
371-
.Where(kv => kv.Value.Context.Pointer == realmPtr)
371+
.Where(kv => kv.Key.RealmPtr == realmPtr)
372372
.Select(kv => kv.Key)
373373
.ToList();
374374

375-
foreach (var typeKey in classesToRemove)
375+
foreach (var key in classesToRemove)
376376
{
377-
if (JSClassRegistry.TryRemove(typeKey, out var jsClass))
377+
if (JSClassRegistry.TryRemove(key, out var jsClass))
378378
{
379379
try
380380
{
381381
jsClass.Dispose();
382382
}
383383
catch (Exception ex)
384384
{
385-
throw new HakoException($"Error disposing JSClass '{typeKey}' for realm {realmPtr}", ex);
385+
throw new HakoException($"Error disposing JSClass '{key.TypeKey}' for realm {realmPtr}", ex);
386386
}
387387
}
388388
}

0 commit comments

Comments
 (0)