Skip to content

Commit 5e26bf1

Browse files
committed
BridgeJS: Register core generic type handles in JavaScriptKit
1 parent 700fe6c commit 5e26bf1

108 files changed

Lines changed: 284 additions & 564 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: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ public struct BridgeJSLink {
348348
declarations.append(" return;")
349349
declarations.append(" }")
350350
declarations.append(" __bjs_typeHandlesRegistered = true;")
351+
// The core (primitive) handles live in the JavaScriptKit library, so
352+
// they are registered once here rather than by every module.
353+
declarations.append(
354+
" \(JSGlueVariableScope.reservedInstance).exports[\"\(ABINameGenerator.coreTypeRegistrationFunctionName)\"]();"
355+
)
351356
for skeleton in skeletons {
352357
guard skeleton.typeRegistrationEntries != nil else { continue }
353358
let name = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
@@ -426,46 +431,70 @@ public struct BridgeJSLink {
426431
)
427432
}
428433

434+
/// Pairs the type IDs Swift pushed with codecs in the matching skeleton order.
435+
private func writeTypeHandleRegistrationBody(into printer: CodeFragmentPrinter) {
436+
printer.write(
437+
"const typeIds = new Int32Array(\(JSGlueVariableScope.reservedMemory).buffer, base >>> 0, count >>> 0);"
438+
)
439+
printer.write("for (let i = 0; i < count; i++) {")
440+
printer.indent {
441+
printer.write("\(JSGlueVariableScope.reservedCodecByTypeId).set(typeIds[i], codecs[i]);")
442+
}
443+
printer.write("}")
444+
}
445+
446+
/// Installs the `bjs_core_register_type_handles` hook. The core handles are
447+
/// owned by the JavaScriptKit library rather than by generated code, so the
448+
/// wasm import exists in every binary that links JavaScriptKit and the hook
449+
/// is always installed; without generics anywhere in the build it is a no-op
450+
/// and the registration export is never called.
451+
private func generateCoreTypeRegistrationHook(into printer: CodeFragmentPrinter) throws {
452+
let hookName = ABINameGenerator.coreTypeRegistrationFunctionName
453+
guard hasGenerics else {
454+
printer.write("bjs[\"\(hookName)\"] = function() {};")
455+
return
456+
}
457+
try ContainerCodecJS.registerPrimitiveCodecs(context: makeCodecPrintContext(printer: printer))
458+
printer.write("bjs[\"\(hookName)\"] = function(base, count) {")
459+
printer.indent {
460+
// Same canonical order as `_bjs_core_register_type_handles` in the
461+
// JavaScriptKit library.
462+
printer.write("const codecs = [")
463+
printer.indent {
464+
for primitive in BridgeType.genericBridgeablePrimitives {
465+
printer.write("\(JSGlueVariableScope.reservedPrimitiveCodecs).\(primitive.token),")
466+
}
467+
}
468+
printer.write("];")
469+
writeTypeHandleRegistrationBody(into: printer)
470+
}
471+
printer.write("}")
472+
}
473+
429474
/// Installs the per-module `bjs_<Module>_register_type_handles` import
430475
/// hooks. A module with a registration function always carries the wasm
431476
/// import, so a hook is always installed; without generics anywhere in the
432477
/// build it is a no-op and the registration export is never called.
433478
private func generateTypeRegistrationHooks(into printer: CodeFragmentPrinter) throws {
479+
try generateCoreTypeRegistrationHook(into: printer)
434480
for skeleton in skeletons {
435-
guard skeleton.typeRegistrationEntries != nil else { continue }
481+
guard let moduleEntries = skeleton.typeRegistrationEntries else { continue }
436482
let hookName = ABINameGenerator.typeRegistrationFunctionName(moduleName: skeleton.moduleName)
437483
guard hasGenerics else {
438484
printer.write("bjs[\"\(hookName)\"] = function() {};")
439485
continue
440486
}
441-
// The hooks resolve type IDs against the shared primitive codec table.
442-
try ContainerCodecJS.registerPrimitiveCodecs(context: makeCodecPrintContext(printer: printer))
443-
let moduleEntries = skeleton.exported?.genericBridgeableTypeEntries ?? []
444487
printer.write("bjs[\"\(hookName)\"] = function(base, count) {")
445488
try printer.indent {
446-
// Same canonical order as the Swift registration function:
447-
// primitives first, then the module's own types.
489+
// Same order as the module's Swift registration function.
448490
printer.write("const codecs = [")
449-
printer.indent {
450-
for primitive in BridgeType.genericBridgeablePrimitives {
451-
printer.write("\(JSGlueVariableScope.reservedPrimitiveCodecs).\(primitive.token),")
452-
}
453-
}
454-
printer.write("].concat([")
455491
try printer.indent {
456492
for entry in moduleEntries {
457493
try appendGenericCodecLiteral(type: entry.bridgeType, into: printer)
458494
}
459495
}
460-
printer.write("]);")
461-
printer.write(
462-
"const typeIds = new Int32Array(\(JSGlueVariableScope.reservedMemory).buffer, base >>> 0, count >>> 0);"
463-
)
464-
printer.write("for (let i = 0; i < count; i++) {")
465-
printer.indent {
466-
printer.write("\(JSGlueVariableScope.reservedCodecByTypeId).set(typeIds[i], codecs[i]);")
467-
}
468-
printer.write("}")
496+
printer.write("];")
497+
writeTypeHandleRegistrationBody(into: printer)
469498
}
470499
printer.write("}")
471500
}

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
@@ -249,9 +249,14 @@ import Testing
249249
let coreEntries = try #require(core.typeRegistrationEntries)
250250
#expect(coreEntries.contains { $0.swiftName == "Vector3D" })
251251

252+
// App exports no @JS types, so it owns no handles: only the core
253+
// (library-owned) registration and Core's own registration run.
254+
#expect(app.typeRegistrationEntries == nil)
255+
252256
let js = try BridgeJSLink(skeletons: [core, app], sharedMemory: false).link().outputJs
257+
#expect(js.contains("instance.exports[\"bjs_core_register_type_handles\"]();"))
253258
#expect(js.contains("instance.exports[\"bjs_Core_register_type_handles\"]();"))
254-
#expect(js.contains("instance.exports[\"bjs_App_register_type_handles\"]();"))
259+
#expect(!js.contains("bjs_App_register_type_handles"))
255260
// `lower(v)` is unique to a codec literal in a registration array;
256261
// `structHelpers.Vector3D` on its own is emitted for every @JS struct.
257262
#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)