Skip to content

Commit c839ea2

Browse files
committed
BridgeJS: Register core generic type handles in JavaScriptKit
1 parent 4aafb00 commit c839ea2

108 files changed

Lines changed: 301 additions & 570 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,21 +2275,6 @@ fileprivate func _bjs_Benchmarks_register_type_handles_extern(_ base: UnsafePoin
22752275
@_expose(wasm, "bjs_Benchmarks_register_type_handles")
22762276
public func _bjs_Benchmarks_register_type_handles() {
22772277
let typeIds: [Int32] = [
2278-
Bool.bridgeJSTypeID,
2279-
Int.bridgeJSTypeID,
2280-
Int8.bridgeJSTypeID,
2281-
UInt8.bridgeJSTypeID,
2282-
Int16.bridgeJSTypeID,
2283-
UInt16.bridgeJSTypeID,
2284-
Int32.bridgeJSTypeID,
2285-
UInt32.bridgeJSTypeID,
2286-
UInt.bridgeJSTypeID,
2287-
Int64.bridgeJSTypeID,
2288-
UInt64.bridgeJSTypeID,
2289-
Float.bridgeJSTypeID,
2290-
Double.bridgeJSTypeID,
2291-
String.bridgeJSTypeID,
2292-
JSValue.bridgeJSTypeID,
22932278
SimpleStruct.bridgeJSTypeID,
22942279
Address.bridgeJSTypeID,
22952280
Person.bridgeJSTypeID,

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -295,21 +295,6 @@ fileprivate func _bjs_PlayBridgeJS_register_type_handles_extern(_ base: UnsafePo
295295
@_expose(wasm, "bjs_PlayBridgeJS_register_type_handles")
296296
public func _bjs_PlayBridgeJS_register_type_handles() {
297297
let typeIds: [Int32] = [
298-
Bool.bridgeJSTypeID,
299-
Int.bridgeJSTypeID,
300-
Int8.bridgeJSTypeID,
301-
UInt8.bridgeJSTypeID,
302-
Int16.bridgeJSTypeID,
303-
UInt16.bridgeJSTypeID,
304-
Int32.bridgeJSTypeID,
305-
UInt32.bridgeJSTypeID,
306-
UInt.bridgeJSTypeID,
307-
Int64.bridgeJSTypeID,
308-
UInt64.bridgeJSTypeID,
309-
Float.bridgeJSTypeID,
310-
Double.bridgeJSTypeID,
311-
String.bridgeJSTypeID,
312-
JSValue.bridgeJSTypeID,
313298
PlayBridgeJSOutput.bridgeJSTypeID,
314299
PlayBridgeJSDiagnostic.bridgeJSTypeID,
315300
PlayBridgeJSResult.bridgeJSTypeID,

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,10 @@ struct GenericConformanceCodegen {
908908
/// registered type's `bridgeJSTypeID` into a buffer, in the canonical order of
909909
/// `BridgeJSSkeleton.typeRegistrationEntries`, and passes it to the JS import
910910
/// hook of the same name, which pairs the IDs with its codec array by index.
911+
///
912+
/// Only the module's own `@JS` types are listed; the core (primitive) handles are
913+
/// registered once by the JavaScriptKit library itself
914+
/// (`_bjs_core_register_type_handles`).
911915
public struct GenericTypeRegistrationCodegen {
912916
public init() {}
913917

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@ public struct BridgeJSLink {
355355
declarations.append(" return;")
356356
declarations.append(" }")
357357
declarations.append(" __bjs_typeHandlesRegistered = true;")
358+
// The core (primitive) handles live in the JavaScriptKit library, so
359+
// they are registered once here rather than by every module.
360+
declarations.append(
361+
" \(JSGlueVariableScope.reservedInstance).exports[\"\(ABINameGenerator.coreTypeRegistrationFunctionName)\"]();"
362+
)
358363
for skeleton in skeletons {
359364
guard skeleton.typeRegistrationEntries != nil else { continue }
360365
let name = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
@@ -433,53 +438,88 @@ public struct BridgeJSLink {
433438
)
434439
}
435440

441+
/// Writes the body shared by every registration hook: verify the codec array
442+
/// lines up with the buffer Swift pushed, then pair IDs with codecs by index.
443+
///
444+
/// The count check is the enforcement point of the ordering contract: the
445+
/// Swift side and the JS side derive their lists from the same declaration
446+
/// order, so a divergence shows up here instead of as a silent mismatch.
447+
private func writeTypeHandleRegistrationBody(
448+
into printer: CodeFragmentPrinter,
449+
mismatchDescription: String
450+
) {
451+
printer.write("if (count !== codecs.length) {")
452+
printer.indent {
453+
printer.write(
454+
"throw new Error(\"BridgeJS: type handle registration mismatch for \(mismatchDescription)\");"
455+
)
456+
}
457+
printer.write("}")
458+
printer.write(
459+
"const typeIds = new Int32Array(\(JSGlueVariableScope.reservedMemory).buffer, base >>> 0, count >>> 0);"
460+
)
461+
printer.write("for (let i = 0; i < count; i++) {")
462+
printer.indent {
463+
printer.write("\(JSGlueVariableScope.reservedCodecByTypeId).set(typeIds[i], codecs[i]);")
464+
}
465+
printer.write("}")
466+
}
467+
468+
/// Installs the `bjs_core_register_type_handles` hook. The core handles are
469+
/// owned by the JavaScriptKit library rather than by generated code, so the
470+
/// wasm import exists in every binary that links JavaScriptKit and the hook
471+
/// is always installed; without generics anywhere in the build it is a no-op
472+
/// and the registration export is never called.
473+
private func generateCoreTypeRegistrationHook(into printer: CodeFragmentPrinter) throws {
474+
let hookName = ABINameGenerator.coreTypeRegistrationFunctionName
475+
guard hasGenerics else {
476+
printer.write("bjs[\"\(hookName)\"] = function() {};")
477+
return
478+
}
479+
try ContainerCodecJS.registerPrimitiveCodecs(context: makeCodecPrintContext(printer: printer))
480+
printer.write("bjs[\"\(hookName)\"] = function(base, count) {")
481+
printer.indent {
482+
// Same canonical order as `_bjs_core_register_type_handles` in the
483+
// JavaScriptKit library.
484+
printer.write("const codecs = [")
485+
printer.indent {
486+
for primitive in BridgeType.genericBridgeablePrimitives {
487+
printer.write("\(JSGlueVariableScope.reservedPrimitiveCodecs).\(primitive.token),")
488+
}
489+
}
490+
printer.write("];")
491+
writeTypeHandleRegistrationBody(into: printer, mismatchDescription: "core types")
492+
}
493+
printer.write("}")
494+
}
495+
436496
/// Installs the per-module `bjs_<Module>_register_type_handles` import
437497
/// hooks. A module with a registration function always carries the wasm
438498
/// import, so a hook is always installed; without generics anywhere in the
439499
/// build it is a no-op and the registration export is never called.
440500
private func generateTypeRegistrationHooks(into printer: CodeFragmentPrinter) throws {
501+
try generateCoreTypeRegistrationHook(into: printer)
441502
for skeleton in skeletons {
442-
guard skeleton.typeRegistrationEntries != nil else { continue }
503+
guard let moduleEntries = skeleton.typeRegistrationEntries else { continue }
443504
let hookName = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
444505
guard hasGenerics else {
445506
printer.write("bjs[\"\(hookName)\"] = function() {};")
446507
continue
447508
}
448-
// The hooks resolve type IDs against the shared primitive codec table.
449-
try ContainerCodecJS.registerPrimitiveCodecs(context: makeCodecPrintContext(printer: printer))
450-
let moduleEntries = skeleton.exported?.genericBridgeableTypeEntries ?? []
451509
printer.write("bjs[\"\(hookName)\"] = function(base, count) {")
452510
try printer.indent {
453-
// Same canonical order as the Swift registration function:
454-
// primitives first, then the module's own types.
511+
// Same order as the module's Swift registration function.
455512
printer.write("const codecs = [")
456-
printer.indent {
457-
for primitive in BridgeType.genericBridgeablePrimitives {
458-
printer.write("\(JSGlueVariableScope.reservedPrimitiveCodecs).\(primitive.token),")
459-
}
460-
}
461-
printer.write("].concat([")
462513
try printer.indent {
463514
for entry in moduleEntries {
464515
try appendGenericCodecLiteral(type: entry.bridgeType, into: printer)
465516
}
466517
}
467-
printer.write("]);")
468-
printer.write("if (count !== codecs.length) {")
469-
printer.indent {
470-
printer.write(
471-
"throw new Error(\"BridgeJS: type handle registration mismatch for module '\(skeleton.moduleName)'\");"
472-
)
473-
}
474-
printer.write("}")
475-
printer.write(
476-
"const typeIds = new Int32Array(\(JSGlueVariableScope.reservedMemory).buffer, base >>> 0, count >>> 0);"
518+
printer.write("];")
519+
writeTypeHandleRegistrationBody(
520+
into: printer,
521+
mismatchDescription: "module '\(skeleton.moduleName)'"
477522
)
478-
printer.write("for (let i = 0; i < count; i++) {")
479-
printer.indent {
480-
printer.write("\(JSGlueVariableScope.reservedCodecByTypeId).set(typeIds[i], codecs[i]);")
481-
}
482-
printer.write("}")
483523
}
484524
printer.write("}")
485525
}

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ public struct ABINameGenerator {
3232
"bjs_\(moduleName)_register_type_handles"
3333
}
3434

35+
/// Name of the core type-handle registration function. Unlike the per-module
36+
/// ones, this is defined once in the JavaScriptKit library (see
37+
/// `_bjs_core_register_type_handles` in `BridgeJSIntrinsics.swift`) so the
38+
/// primitive handles exist exactly once in the final binary and the JS glue
39+
/// registers their codecs once per linked bundle.
40+
public static let coreTypeRegistrationFunctionName = "bjs_core_register_type_handles"
41+
3542
/// Generates ABI name using standardized namespace + context pattern
3643
public static func generateABIName(
3744
baseName: String,
@@ -383,17 +390,17 @@ extension ExportedSkeleton {
383390

384391
extension BridgeJSSkeleton {
385392
/// The ordered list of types this module registers type handles for, or
386-
/// `nil` when it emits no registration function. Primitive handles are
387-
/// library singletons, so every module re-registering them writes the same
388-
/// ID-to-codec pair, and a pure-import build still gets a populated table.
393+
/// `nil` when it emits no registration function.
394+
///
395+
/// Only the module's own `@JS` types appear here: the core (primitive)
396+
/// handles are owned by the JavaScriptKit library, which registers them once
397+
/// for the whole binary via ``ABINameGenerator/coreTypeRegistrationFunctionName``.
398+
/// A module that only *uses* generics therefore needs no registration
399+
/// function of its own.
389400
public var typeRegistrationEntries: [GenericBridgeableTypeEntry]? {
390401
let exportedEntries = exported?.genericBridgeableTypeEntries ?? []
391-
let hasGenericImports = imported?.hasGenericDeclarations ?? false
392-
guard !exportedEntries.isEmpty || hasGenericImports else { return nil }
393-
let primitives = BridgeType.genericBridgeablePrimitives.map {
394-
GenericBridgeableTypeEntry(swiftName: $0.token, bridgeType: $0.type)
395-
}
396-
return primitives + exportedEntries
402+
guard !exportedEntries.isEmpty else { return nil }
403+
return exportedEntries
397404
}
398405
}
399406

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,14 @@ import Testing
251251
let coreEntries = try #require(core.typeRegistrationEntries)
252252
#expect(coreEntries.contains { $0.swiftName == "Vector3D" })
253253

254+
// App exports no @JS types, so it owns no handles: only the core
255+
// (library-owned) registration and Core's own registration run.
256+
#expect(app.typeRegistrationEntries == nil)
257+
254258
let js = try BridgeJSLink(skeletons: [core, app], sharedMemory: false).link().outputJs
259+
#expect(js.contains("instance.exports[\"bjs_core_register_type_handles\"]();"))
255260
#expect(js.contains("instance.exports[\"bjs_Core_register_type_handles\"]();"))
256-
#expect(js.contains("instance.exports[\"bjs_App_register_type_handles\"]();"))
261+
#expect(!js.contains("bjs_App_register_type_handles"))
257262
// `lower(v)` is unique to a codec literal in a registration array;
258263
// `structHelpers.Vector3D` on its own is emitted for every @JS struct.
259264
#expect(js.contains("structHelpers.Vector3D.lower(v);"))
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import BridgeJSSkeleton
5+
6+
/// The core (primitive) generic type handles are registered by the JavaScriptKit
7+
/// library itself, not by generated code, so the ordering contract between the
8+
/// Swift buffer and the JS codec array spans two repositories' worth of source:
9+
/// `_bjs_core_register_type_handles` in `Sources/JavaScriptKit/BridgeJSIntrinsics.swift`
10+
/// and `BridgeType.genericBridgeablePrimitives` here.
11+
///
12+
/// The generated glue checks the *count* at registration time; this test checks
13+
/// the *order* at build time so a reordering cannot silently mis-pair codecs.
14+
@Suite struct CoreTypeRegistrationContractTests {
15+
private static var repositoryRoot: URL {
16+
URL(fileURLWithPath: #filePath)
17+
.deletingLastPathComponent() // BridgeJSToolTests
18+
.deletingLastPathComponent() // Tests
19+
.deletingLastPathComponent() // BridgeJS
20+
.deletingLastPathComponent() // Plugins
21+
.deletingLastPathComponent() // <repo root>
22+
}
23+
24+
@Test
25+
func coreTypeHandleOrderMatchesGenericBridgeablePrimitives() throws {
26+
let intrinsics = Self.repositoryRoot
27+
.appendingPathComponent("Sources/JavaScriptKit/BridgeJSIntrinsics.swift")
28+
let source = try String(contentsOf: intrinsics, encoding: .utf8)
29+
30+
let beginMarker = "// BEGIN bjs_core_type_handles"
31+
let endMarker = "// END bjs_core_type_handles"
32+
guard let begin = source.range(of: beginMarker), let end = source.range(of: endMarker) else {
33+
Issue.record("Could not find the core type handle list markers in \(intrinsics.path)")
34+
return
35+
}
36+
37+
let names =
38+
source[begin.upperBound..<end.lowerBound]
39+
.split(separator: "\n")
40+
.compactMap { line -> String? in
41+
let trimmed = line.trimmingCharacters(in: .whitespaces)
42+
guard trimmed.hasSuffix(".bridgeJSTypeID,") else { return nil }
43+
return String(trimmed.dropLast(".bridgeJSTypeID,".count))
44+
}
45+
46+
#expect(names == BridgeType.genericBridgeablePrimitives.map(\.token))
47+
}
48+
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/GenericMethodOnlyModuleCodegenTests.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import Testing
33
@Suite struct GenericMethodOnlyModuleCodegenTests {
44
@Test
55
func importMethodOnlyEmitsJSRuntimeInfrastructure() throws {
6-
// A module with generic imports but no exported @JS types still needs a
7-
// populated codec table for the primitives, so it registers them itself.
6+
// A module with generic imports but no exported @JS types needs a
7+
// populated codec table, but it owns none of the entries: the core
8+
// handles come from the JavaScriptKit library's own registration, so the
9+
// module emits no registration function of its own.
810
let js = try linkSource(
911
"""
1012
@JSClass struct OnlyConsumer {
@@ -14,8 +16,9 @@ import Testing
1416
).js
1517
#expect(js.contains("const __bjs_codecByTypeId = new Map();"))
1618
#expect(js.contains("function __bjs_codecForTypeId(typeId) {"))
17-
#expect(js.contains("bjs[\"bjs_TestModule_register_type_handles\"] = function(base, count) {"))
18-
#expect(js.contains("instance.exports[\"bjs_TestModule_register_type_handles\"]();"))
19+
#expect(js.contains("bjs[\"bjs_core_register_type_handles\"] = function(base, count) {"))
20+
#expect(js.contains("instance.exports[\"bjs_core_register_type_handles\"]();"))
21+
#expect(!js.contains("bjs_TestModule_register_type_handles"))
1922
}
2023

2124
@Test
@@ -33,7 +36,28 @@ import Testing
3336
).js
3437
#expect(js.contains("const __bjs_codecByTypeId = new Map();"))
3538
#expect(js.contains("function __bjs_codecForTypeId(typeId) {"))
39+
#expect(js.contains("bjs[\"bjs_core_register_type_handles\"] = function(base, count) {"))
40+
#expect(js.contains("instance.exports[\"bjs_core_register_type_handles\"]();"))
41+
#expect(!js.contains("bjs_TestModule_register_type_handles"))
42+
}
43+
44+
@Test
45+
func exportedTypesStillRegisterTheirOwnHandlesOnly() throws {
46+
// A module that exports @JS types registers exactly those, without
47+
// repeating the core entries the library already owns.
48+
let js = try linkSource(
49+
"""
50+
@JS struct Point {
51+
var x: Int
52+
@JS init(x: Int) { self.x = x }
53+
}
54+
@JSClass struct Consumer {
55+
@JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
56+
}
57+
"""
58+
).js
3659
#expect(js.contains("bjs[\"bjs_TestModule_register_type_handles\"] = function(base, count) {"))
37-
#expect(js.contains("instance.exports[\"bjs_TestModule_register_type_handles\"]();"))
60+
// The primitive entries appear exactly once, in the core hook.
61+
#expect(js.components(separatedBy: "__bjs_primitiveCodecs.Bool,").count - 1 == 1)
3862
}
3963
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -415,21 +415,6 @@ fileprivate func _bjs_TestModule_register_type_handles_extern(_ base: UnsafePoin
415415
@_expose(wasm, "bjs_TestModule_register_type_handles")
416416
public func _bjs_TestModule_register_type_handles() {
417417
let typeIds: [Int32] = [
418-
Bool.bridgeJSTypeID,
419-
Int.bridgeJSTypeID,
420-
Int8.bridgeJSTypeID,
421-
UInt8.bridgeJSTypeID,
422-
Int16.bridgeJSTypeID,
423-
UInt16.bridgeJSTypeID,
424-
Int32.bridgeJSTypeID,
425-
UInt32.bridgeJSTypeID,
426-
UInt.bridgeJSTypeID,
427-
Int64.bridgeJSTypeID,
428-
UInt64.bridgeJSTypeID,
429-
Float.bridgeJSTypeID,
430-
Double.bridgeJSTypeID,
431-
String.bridgeJSTypeID,
432-
JSValue.bridgeJSTypeID,
433418
PolygonReference.bridgeJSTypeID,
434419
TagReference.bridgeJSTypeID,
435420
InnerTag.bridgeJSTypeID,

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/AliasInClosure.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -200,21 +200,6 @@ fileprivate func _bjs_TestModule_register_type_handles_extern(_ base: UnsafePoin
200200
@_expose(wasm, "bjs_TestModule_register_type_handles")
201201
public func _bjs_TestModule_register_type_handles() {
202202
let typeIds: [Int32] = [
203-
Bool.bridgeJSTypeID,
204-
Int.bridgeJSTypeID,
205-
Int8.bridgeJSTypeID,
206-
UInt8.bridgeJSTypeID,
207-
Int16.bridgeJSTypeID,
208-
UInt16.bridgeJSTypeID,
209-
Int32.bridgeJSTypeID,
210-
UInt32.bridgeJSTypeID,
211-
UInt.bridgeJSTypeID,
212-
Int64.bridgeJSTypeID,
213-
UInt64.bridgeJSTypeID,
214-
Float.bridgeJSTypeID,
215-
Double.bridgeJSTypeID,
216-
String.bridgeJSTypeID,
217-
JSValue.bridgeJSTypeID,
218203
PolygonReference.bridgeJSTypeID,
219204
]
220205
typeIds.withUnsafeBufferPointer { buffer in

0 commit comments

Comments
 (0)