Skip to content

Commit db10037

Browse files
committed
BridgeJS: Register core generic type handles from JavaScriptKit
Every module's generated registration function listed the 15 core type handles before its own types, and every module's JS registration hook repeated the 15 primitive codec entries. The core types are owned by the library, not by any module, so define their registration once. `_bjs_core_register_type_handles` in JavaScriptKit exposes the core handle buffer under `bjs_core_register_type_handles`; because JavaScriptKit is linked into every BridgeJS binary, exactly one definition exists in the final wasm. The link step emits the matching `bjs[...]` hook once per linked bundle instead of once per module, and drives it before the per-module hooks. Generated per-module registration now carries only that module's own `@JS` types, so a module that merely uses generics emits no registration function at all. The ordering contract is still enforced on both sides: each hook checks the codec array length against the count Swift pushed, and a new build-time test checks the library's core list against `BridgeType.genericBridgeablePrimitives`, which the link step uses to build the core codec array.
1 parent 96479bd commit db10037

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)