Skip to content

Commit 6310307

Browse files
Merge pull request #456 from PassiveLogic/feat/protocol-support-methods
BridgeJS: @js Protocol with methods support
2 parents 237097a + 7efb593 commit 6310307

File tree

37 files changed

+2612
-107
lines changed

37 files changed

+2612
-107
lines changed

Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,5 +720,8 @@
720720
}
721721
}
722722
],
723-
"moduleName" : "Benchmarks"
723+
"moduleName" : "Benchmarks",
724+
"protocols" : [
725+
726+
]
724727
}

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,8 @@
138138
"functions" : [
139139

140140
],
141-
"moduleName" : "PlayBridgeJS"
141+
"moduleName" : "PlayBridgeJS",
142+
"protocols" : [
143+
144+
]
142145
}

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 343 additions & 79 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ extension BridgeType {
435435
case .void: return .void
436436
case .swiftHeapObject:
437437
throw BridgeJSCoreError("swiftHeapObject is not supported in imported signatures")
438+
case .swiftProtocol:
439+
throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures")
438440
case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum:
439441
throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports")
440442
case .optional:
@@ -465,6 +467,8 @@ extension BridgeType {
465467
case .void: return .void
466468
case .swiftHeapObject:
467469
throw BridgeJSCoreError("swiftHeapObject is not supported in imported signatures")
470+
case .swiftProtocol:
471+
throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures")
468472
case .caseEnum, .rawValueEnum, .associatedValueEnum, .namespaceEnum:
469473
throw BridgeJSCoreError("Enum types are not yet supported in TypeScript imports")
470474
case .optional:

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,9 @@ struct BridgeJSLink {
240240
enumPropertyPrinter.write("},")
241241

242242
if !property.isReadonly {
243-
let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
243+
let setterThunkBuilder = ExportedThunkBuilder(
244+
effects: Effects(isAsync: false, isThrows: false)
245+
)
244246
try setterThunkBuilder.lowerParameter(
245247
param: Parameter(label: "value", name: "value", type: property.type)
246248
)
@@ -300,6 +302,29 @@ struct BridgeJSLink {
300302
data.importObjectBuilders.append(importObjectBuilder)
301303
}
302304

305+
for skeleton in exportedSkeletons {
306+
if !skeleton.protocols.isEmpty {
307+
let importObjectBuilder: ImportObjectBuilder
308+
if let existingBuilder = data.importObjectBuilders.first(where: { $0.moduleName == skeleton.moduleName }
309+
) {
310+
importObjectBuilder = existingBuilder
311+
} else {
312+
importObjectBuilder = ImportObjectBuilder(moduleName: skeleton.moduleName)
313+
data.importObjectBuilders.append(importObjectBuilder)
314+
}
315+
316+
for proto in skeleton.protocols {
317+
for method in proto.methods {
318+
try renderProtocolMethod(
319+
importObjectBuilder: importObjectBuilder,
320+
protocol: proto,
321+
method: method
322+
)
323+
}
324+
}
325+
}
326+
}
327+
303328
return data
304329
}
305330

@@ -572,6 +597,22 @@ struct BridgeJSLink {
572597
"""
573598
let printer = CodeFragmentPrinter(header: header)
574599
printer.nextLine()
600+
601+
for skeleton in exportedSkeletons {
602+
for proto in skeleton.protocols {
603+
printer.write("export interface \(proto.name) {")
604+
printer.indent {
605+
for method in proto.methods {
606+
printer.write(
607+
"\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: method.effects));"
608+
)
609+
}
610+
}
611+
printer.write("}")
612+
printer.nextLine()
613+
}
614+
}
615+
575616
printer.write(lines: data.topLevelDtsEnumLines)
576617

577618
// Generate Object types for const-style enums
@@ -1468,7 +1509,9 @@ extension BridgeJSLink {
14681509

14691510
if !property.isReadonly {
14701511
// Generate setter
1471-
let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1512+
let setterThunkBuilder = ExportedThunkBuilder(
1513+
effects: Effects(isAsync: false, isThrows: false)
1514+
)
14721515
try setterThunkBuilder.lowerParameter(
14731516
param: Parameter(label: "value", name: "value", type: property.type)
14741517
)
@@ -1539,7 +1582,9 @@ extension BridgeJSLink {
15391582

15401583
if !property.isReadonly {
15411584
// Generate setter
1542-
let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1585+
let setterThunkBuilder = ExportedThunkBuilder(
1586+
effects: Effects(isAsync: false, isThrows: false)
1587+
)
15431588
try setterThunkBuilder.lowerParameter(
15441589
param: Parameter(label: "value", name: "value", type: property.type)
15451590
)
@@ -1704,7 +1749,9 @@ extension BridgeJSLink {
17041749

17051750
// Generate static property setter if not readonly
17061751
if !property.isReadonly {
1707-
let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1752+
let setterThunkBuilder = ExportedThunkBuilder(
1753+
effects: Effects(isAsync: false, isThrows: false)
1754+
)
17081755
try setterThunkBuilder.lowerParameter(
17091756
param: Parameter(label: "value", name: "value", type: property.type)
17101757
)
@@ -1752,7 +1799,9 @@ extension BridgeJSLink {
17521799

17531800
// Generate instance property setter if not readonly
17541801
if !property.isReadonly {
1755-
let setterThunkBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false))
1802+
let setterThunkBuilder = ExportedThunkBuilder(
1803+
effects: Effects(isAsync: false, isThrows: false)
1804+
)
17561805
setterThunkBuilder.lowerSelf()
17571806
try setterThunkBuilder.lowerParameter(
17581807
param: Parameter(label: "value", name: "value", type: property.type)
@@ -2367,6 +2416,25 @@ extension BridgeJSLink {
23672416
)
23682417
return (funcLines, [])
23692418
}
2419+
2420+
func renderProtocolMethod(
2421+
importObjectBuilder: ImportObjectBuilder,
2422+
protocol: ExportedProtocol,
2423+
method: ExportedFunction
2424+
) throws {
2425+
let thunkBuilder = ImportedThunkBuilder()
2426+
thunkBuilder.liftSelf()
2427+
for param in method.parameters {
2428+
try thunkBuilder.liftParameter(param: param)
2429+
}
2430+
let returnExpr = try thunkBuilder.callMethod(name: method.name, returnType: method.returnType)
2431+
let funcLines = thunkBuilder.renderFunction(
2432+
name: method.abiName,
2433+
returnExpr: returnExpr,
2434+
returnType: method.returnType
2435+
)
2436+
importObjectBuilder.assignToImportObject(name: method.abiName, function: funcLines)
2437+
}
23702438
}
23712439

23722440
struct BridgeJSLinkError: Error {
@@ -2402,6 +2470,8 @@ extension BridgeType {
24022470
return "\(name)Tag"
24032471
case .namespaceEnum(let name):
24042472
return name
2473+
case .swiftProtocol(let name):
2474+
return name
24052475
}
24062476
}
24072477
}

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,25 @@ struct IntrinsicJSFragment: Sendable {
297297
switch wrappedType {
298298
case .swiftHeapObject:
299299
return ["+\(isSomeVar)", "\(isSomeVar) ? \(value).pointer : 0"]
300+
case .swiftProtocol:
301+
return [
302+
"+\(isSomeVar)",
303+
"\(isSomeVar) ? \(JSGlueVariableScope.reservedSwift).memory.retain(\(value)) : 0",
304+
]
305+
case .jsObject:
306+
let idVar = scope.variable("id")
307+
printer.write("let \(idVar);")
308+
printer.write("if (\(isSomeVar)) {")
309+
printer.indent {
310+
printer.write("\(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(value));")
311+
}
312+
printer.write("}")
313+
cleanupCode.write("if (\(idVar) !== undefined) {")
314+
cleanupCode.indent {
315+
cleanupCode.write("\(JSGlueVariableScope.reservedSwift).memory.release(\(idVar));")
316+
}
317+
cleanupCode.write("}")
318+
return ["+\(isSomeVar)", "\(isSomeVar) ? \(idVar) : 0"]
300319
default:
301320
return ["+\(isSomeVar)", "\(isSomeVar) ? \(value) : 0"]
302321
}
@@ -326,6 +345,9 @@ struct IntrinsicJSFragment: Sendable {
326345
case .string:
327346
printer.write("const \(resultVar) = \(JSGlueVariableScope.reservedStorageToReturnString);")
328347
printer.write("\(JSGlueVariableScope.reservedStorageToReturnString) = undefined;")
348+
case .jsObject, .swiftProtocol:
349+
printer.write("const \(resultVar) = \(JSGlueVariableScope.reservedStorageToReturnString);")
350+
printer.write("\(JSGlueVariableScope.reservedStorageToReturnString) = undefined;")
329351
case .swiftHeapObject(let className):
330352
let pointerVar = scope.variable("pointer")
331353
printer.write(
@@ -397,6 +419,7 @@ struct IntrinsicJSFragment: Sendable {
397419
case .jsObject: return .jsObjectLowerParameter
398420
case .swiftHeapObject:
399421
return .swiftHeapObjectLowerParameter
422+
case .swiftProtocol: return .jsObjectLowerParameter
400423
case .void: return .void
401424
case .optional(let wrappedType):
402425
return try .optionalLowerParameter(wrappedType: wrappedType)
@@ -422,6 +445,7 @@ struct IntrinsicJSFragment: Sendable {
422445
case .string: return .stringLiftReturn
423446
case .jsObject: return .jsObjectLiftReturn
424447
case .swiftHeapObject(let name): return .swiftHeapObjectLiftReturn(name)
448+
case .swiftProtocol: return .jsObjectLiftReturn
425449
case .void: return .void
426450
case .optional(let wrappedType): return .optionalLiftReturn(wrappedType: wrappedType)
427451
case .caseEnum: return .identity
@@ -455,6 +479,7 @@ struct IntrinsicJSFragment: Sendable {
455479
message:
456480
"Swift heap objects are not supported to be passed as parameters to imported JS functions: \(name)"
457481
)
482+
case .swiftProtocol: return .jsObjectLiftParameter
458483
case .void:
459484
throw BridgeJSLinkError(
460485
message: "Void can't appear in parameters of imported JS functions"
@@ -494,6 +519,7 @@ struct IntrinsicJSFragment: Sendable {
494519
throw BridgeJSLinkError(
495520
message: "Swift heap objects are not supported to be returned from imported JS functions"
496521
)
522+
case .swiftProtocol: return .jsObjectLowerReturn
497523
case .void: return .void
498524
case .optional(let wrappedType):
499525
throw BridgeJSLinkError(

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public enum BridgeType: Codable, Equatable, Sendable {
7474
case rawValueEnum(String, SwiftEnumRawType)
7575
case associatedValueEnum(String)
7676
case namespaceEnum(String)
77+
case swiftProtocol(String)
7778
}
7879

7980
public enum WasmCoreType: String, Codable, Sendable {
@@ -269,6 +270,18 @@ public enum EnumType: String, Codable, Sendable {
269270

270271
// MARK: - Exported Skeleton
271272

273+
public struct ExportedProtocol: Codable, Equatable {
274+
public let name: String
275+
public let methods: [ExportedFunction]
276+
public let namespace: [String]?
277+
278+
public init(name: String, methods: [ExportedFunction], namespace: [String]? = nil) {
279+
self.name = name
280+
self.methods = methods
281+
self.namespace = namespace
282+
}
283+
}
284+
272285
public struct ExportedFunction: Codable, Equatable, Sendable {
273286
public var name: String
274287
public var abiName: String
@@ -407,12 +420,20 @@ public struct ExportedSkeleton: Codable {
407420
public let functions: [ExportedFunction]
408421
public let classes: [ExportedClass]
409422
public let enums: [ExportedEnum]
423+
public let protocols: [ExportedProtocol]
410424

411-
public init(moduleName: String, functions: [ExportedFunction], classes: [ExportedClass], enums: [ExportedEnum]) {
425+
public init(
426+
moduleName: String,
427+
functions: [ExportedFunction],
428+
classes: [ExportedClass],
429+
enums: [ExportedEnum],
430+
protocols: [ExportedProtocol] = []
431+
) {
412432
self.moduleName = moduleName
413433
self.functions = functions
414434
self.classes = classes
415435
self.enums = enums
436+
self.protocols = protocols
416437
}
417438
}
418439

@@ -514,6 +535,9 @@ extension BridgeType {
514535
return nil
515536
case .namespaceEnum:
516537
return nil
538+
case .swiftProtocol:
539+
// Protocols pass JSObject IDs as Int32
540+
return .i32
517541
}
518542
}
519543

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import JavaScriptKit
2+
3+
@JS protocol MyViewControllerDelegate {
4+
func onSomethingHappened()
5+
func onValueChanged(_ value: String)
6+
func onCountUpdated(count: Int) -> Bool
7+
func onLabelUpdated(_ prefix: String, _ suffix: String)
8+
func isCountEven() -> Bool
9+
}
10+
11+
@JS class MyViewController {
12+
@JS
13+
var delegate: MyViewControllerDelegate
14+
15+
@JS
16+
var secondDelegate: MyViewControllerDelegate?
17+
18+
@JS init(delegate: MyViewControllerDelegate) {
19+
self.delegate = delegate
20+
}
21+
22+
@JS func triggerEvent() {
23+
delegate.onSomethingHappened()
24+
}
25+
26+
@JS func updateValue(_ value: String) {
27+
delegate.onValueChanged(value)
28+
}
29+
30+
@JS func updateCount(_ count: Int) -> Bool {
31+
return delegate.onCountUpdated(count: count)
32+
}
33+
34+
@JS func updateLabel(_ prefix: String, _ suffix: String) {
35+
delegate.onLabelUpdated(prefix, suffix)
36+
}
37+
38+
@JS func checkEvenCount() -> Bool {
39+
return delegate.isCountEven()
40+
}
41+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
2+
// DO NOT EDIT.
3+
//
4+
// To update this file, just rebuild your project or run
5+
// `swift package bridge-js`.
6+
7+
export interface MyViewControllerDelegate {
8+
onSomethingHappened(): void;
9+
onValueChanged(value: string): void;
10+
onCountUpdated(count: number): boolean;
11+
onLabelUpdated(prefix: string, suffix: string): void;
12+
isCountEven(): boolean;
13+
}
14+
15+
/// Represents a Swift heap object like a class instance or an actor instance.
16+
export interface SwiftHeapObject {
17+
/// Release the heap object.
18+
///
19+
/// Note: Calling this method will release the heap object and it will no longer be accessible.
20+
release(): void;
21+
}
22+
export interface MyViewController extends SwiftHeapObject {
23+
triggerEvent(): void;
24+
updateValue(value: string): void;
25+
updateCount(count: number): boolean;
26+
updateLabel(prefix: string, suffix: string): void;
27+
checkEvenCount(): boolean;
28+
delegate: MyViewControllerDelegate;
29+
secondDelegate: MyViewControllerDelegate | null;
30+
}
31+
export type Exports = {
32+
MyViewController: {
33+
new(delegate: MyViewControllerDelegate): MyViewController;
34+
}
35+
}
36+
export type Imports = {
37+
}
38+
export function createInstantiator(options: {
39+
imports: Imports;
40+
}, swift: any): Promise<{
41+
addImports: (importObject: WebAssembly.Imports) => void;
42+
setInstance: (instance: WebAssembly.Instance) => void;
43+
createExports: (instance: WebAssembly.Instance) => Exports;
44+
}>;

0 commit comments

Comments
 (0)