Skip to content

Commit c93a031

Browse files
committed
BridgeJS: Support optional @jsclass as exported function parameters
1 parent d1e0f95 commit c93a031

11 files changed

Lines changed: 243 additions & 2 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ struct IntrinsicJSFragment: Sendable {
762762
}
763763

764764
let innerFragment =
765-
if wrappedType.optionalConvention == .stackABI {
765+
if wrappedType.optionalParameterUsesStackABI {
766766
try stackLowerFragment(elementType: wrappedType)
767767
} else {
768768
try lowerParameter(type: wrappedType)
@@ -779,7 +779,7 @@ struct IntrinsicJSFragment: Sendable {
779779
kind: JSOptionalKind,
780780
innerFragment: IntrinsicJSFragment
781781
) throws -> IntrinsicJSFragment {
782-
let isStackConvention = wrappedType.optionalConvention == .stackABI
782+
let isStackConvention = wrappedType.optionalParameterUsesStackABI
783783

784784
return IntrinsicJSFragment(
785785
parameters: ["value"],
@@ -2696,6 +2696,24 @@ private extension BridgeType {
26962696
}
26972697
}
26982698

2699+
/// Whether an optional of this type pushes its payload onto the bridge stack
2700+
/// when passed as a *parameter*.
2701+
///
2702+
/// This usually matches `optionalConvention == .stackABI`, but `jsObject`
2703+
/// optionals are the exception: their return values travel through the stack
2704+
/// while their parameters use the direct `(isSome, objId)` ABI, matching plain
2705+
/// `Optional<JSObject>` and exported `@JS class` parameters.
2706+
var optionalParameterUsesStackABI: Bool {
2707+
switch self {
2708+
case .jsObject:
2709+
return false
2710+
case .nullable(let wrapped, _):
2711+
return wrapped.optionalParameterUsesStackABI
2712+
default:
2713+
return optionalConvention == .stackABI
2714+
}
2715+
}
2716+
26992717
var nilSentinel: NilSentinel {
27002718
switch self {
27012719
case .swiftProtocol:

Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Optionals.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class OptionalPropertyHolder {
3030

3131
@JS func testOptionalPropertyRoundtrip(_ holder: OptionalPropertyHolder?) -> OptionalPropertyHolder?
3232

33+
// Exported functions taking an optional jsObject use the direct (isSome, objId)
34+
// parameter ABI; the return value travels through the stack ABI.
35+
@JS func roundTripExportedOptionalJSObject(value: JSObject?) -> JSObject?
36+
37+
// Exported function taking/returning an optional imported @JSClass (issue #751).
38+
@JS func roundTripExportedOptionalJSClass(value: WithOptionalJSClass?) -> WithOptionalJSClass?
39+
3340
@JS
3441
func roundTripString(name: String?) -> String? {
3542
return name

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Optionals.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,76 @@
239239
}
240240
}
241241
},
242+
{
243+
"abiName" : "bjs_roundTripExportedOptionalJSObject",
244+
"effects" : {
245+
"isAsync" : false,
246+
"isStatic" : false,
247+
"isThrows" : false
248+
},
249+
"name" : "roundTripExportedOptionalJSObject",
250+
"parameters" : [
251+
{
252+
"label" : "value",
253+
"name" : "value",
254+
"type" : {
255+
"nullable" : {
256+
"_0" : {
257+
"jsObject" : {
258+
259+
}
260+
},
261+
"_1" : "null"
262+
}
263+
}
264+
}
265+
],
266+
"returnType" : {
267+
"nullable" : {
268+
"_0" : {
269+
"jsObject" : {
270+
271+
}
272+
},
273+
"_1" : "null"
274+
}
275+
}
276+
},
277+
{
278+
"abiName" : "bjs_roundTripExportedOptionalJSClass",
279+
"effects" : {
280+
"isAsync" : false,
281+
"isStatic" : false,
282+
"isThrows" : false
283+
},
284+
"name" : "roundTripExportedOptionalJSClass",
285+
"parameters" : [
286+
{
287+
"label" : "value",
288+
"name" : "value",
289+
"type" : {
290+
"nullable" : {
291+
"_0" : {
292+
"jsObject" : {
293+
"_0" : "WithOptionalJSClass"
294+
}
295+
},
296+
"_1" : "null"
297+
}
298+
}
299+
}
300+
],
301+
"returnType" : {
302+
"nullable" : {
303+
"_0" : {
304+
"jsObject" : {
305+
"_0" : "WithOptionalJSClass"
306+
}
307+
},
308+
"_1" : "null"
309+
}
310+
}
311+
},
242312
{
243313
"abiName" : "bjs_roundTripString",
244314
"effects" : {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ public func _bjs_testOptionalPropertyRoundtrip(_ holderIsSome: Int32, _ holderVa
2020
#endif
2121
}
2222

23+
@_expose(wasm, "bjs_roundTripExportedOptionalJSObject")
24+
@_cdecl("bjs_roundTripExportedOptionalJSObject")
25+
public func _bjs_roundTripExportedOptionalJSObject(_ valueIsSome: Int32, _ valueValue: Int32) -> Void {
26+
#if arch(wasm32)
27+
let ret = roundTripExportedOptionalJSObject(value: Optional<JSObject>.bridgeJSLiftParameter(valueIsSome, valueValue))
28+
return ret.bridgeJSLowerReturn()
29+
#else
30+
fatalError("Only available on WebAssembly")
31+
#endif
32+
}
33+
34+
@_expose(wasm, "bjs_roundTripExportedOptionalJSClass")
35+
@_cdecl("bjs_roundTripExportedOptionalJSClass")
36+
public func _bjs_roundTripExportedOptionalJSClass(_ valueIsSome: Int32, _ valueValue: Int32) -> Void {
37+
#if arch(wasm32)
38+
let ret = roundTripExportedOptionalJSClass(value: Optional<WithOptionalJSClass>.bridgeJSLiftParameter(valueIsSome, valueValue))
39+
return ret.bridgeJSLowerReturn()
40+
#else
41+
fatalError("Only available on WebAssembly")
42+
#endif
43+
}
44+
2345
@_expose(wasm, "bjs_roundTripString")
2446
@_cdecl("bjs_roundTripString")
2547
public func _bjs_roundTripString(_ nameIsSome: Int32, _ nameBytes: Int32, _ nameLength: Int32) -> Void {

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export type Exports = {
5050
}
5151
roundTripOptionalClass(value: Greeter | null): Greeter | null;
5252
testOptionalPropertyRoundtrip(holder: OptionalPropertyHolder | null): OptionalPropertyHolder | null;
53+
roundTripExportedOptionalJSObject(value: any | null): any | null;
54+
roundTripExportedOptionalJSClass(value: WithOptionalJSClass | null): WithOptionalJSClass | null;
5355
roundTripString(name: string | null): string | null;
5456
roundTripInt(value: number | null): number | null;
5557
roundTripInt8(value: number | null): number | null;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Optionals.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,48 @@ export async function createInstantiator(options, swift) {
725725
const optResult = pointer === null ? null : OptionalPropertyHolder.__construct(pointer);
726726
return optResult;
727727
},
728+
roundTripExportedOptionalJSObject: function bjs_roundTripExportedOptionalJSObject(value) {
729+
const isSome = value != null;
730+
let result;
731+
if (isSome) {
732+
result = swift.memory.retain(value);
733+
} else {
734+
result = 0;
735+
}
736+
instance.exports.bjs_roundTripExportedOptionalJSObject(+isSome, result);
737+
const isSome1 = i32Stack.pop();
738+
let optResult;
739+
if (isSome1) {
740+
const objId = i32Stack.pop();
741+
const obj = swift.memory.getObject(objId);
742+
swift.memory.release(objId);
743+
optResult = obj;
744+
} else {
745+
optResult = null;
746+
}
747+
return optResult;
748+
},
749+
roundTripExportedOptionalJSClass: function bjs_roundTripExportedOptionalJSClass(value) {
750+
const isSome = value != null;
751+
let result;
752+
if (isSome) {
753+
result = swift.memory.retain(value);
754+
} else {
755+
result = 0;
756+
}
757+
instance.exports.bjs_roundTripExportedOptionalJSClass(+isSome, result);
758+
const isSome1 = i32Stack.pop();
759+
let optResult;
760+
if (isSome1) {
761+
const objId = i32Stack.pop();
762+
const obj = swift.memory.getObject(objId);
763+
swift.memory.release(objId);
764+
optResult = obj;
765+
} else {
766+
optResult = null;
767+
}
768+
return optResult;
769+
},
728770
roundTripString: function bjs_roundTripString(name) {
729771
const isSome = name != null;
730772
let result, result1;

Sources/JavaScriptKit/BridgeJSIntrinsics.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,29 @@ extension _BridgedAsOptional where Wrapped == JSObject {
18261826
}
18271827
}
18281828

1829+
extension _BridgedAsOptional where Wrapped: _JSBridgedClass {
1830+
// `@JSClass` wrappers (`_JSBridgedClass`) bridge an underlying `JSObject`, so an
1831+
// optional wrapper mirrors `Optional<JSObject>`: parameters use the direct
1832+
// (`isSome`, object id) ABI while returns travel through the bridge stack.
1833+
//
1834+
// Stack push/pop is provided by the generic `Wrapped: _BridgedSwiftStackType`
1835+
// extension; only the direct parameter lift and the export return lowering need
1836+
// dedicated implementations here.
1837+
@_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32, _ objectId: Int32) -> Self {
1838+
Self(
1839+
optional: Optional<Wrapped>._bridgeJSLiftParameter(
1840+
isSome,
1841+
objectId,
1842+
liftWrapped: Wrapped.bridgeJSLiftParameter
1843+
)
1844+
)
1845+
}
1846+
1847+
@_spi(BridgeJS) public consuming func bridgeJSLowerReturn() -> Void {
1848+
Wrapped.bridgeJSStackPushAsOptional(asOptional)
1849+
}
1850+
}
1851+
18291852
extension _BridgedAsOptional where Wrapped: _BridgedSwiftProtocolWrapper {
18301853
@_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32, _ objectId: Int32) -> Self {
18311854
Self(

Tests/BridgeJSRuntimeTests/ExportAPITests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func runJsWorks() -> Void
7575
return try Foo(value)
7676
}
7777

78+
@JS func roundTripOptionalImportedClass(v: Foo?) -> Foo? {
79+
return v
80+
}
81+
7882
struct TestError: Error {
7983
let message: String
8084
}

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6940,6 +6940,17 @@ public func _bjs_makeImportedFoo(_ valueBytes: Int32, _ valueLength: Int32) -> I
69406940
#endif
69416941
}
69426942

6943+
@_expose(wasm, "bjs_roundTripOptionalImportedClass")
6944+
@_cdecl("bjs_roundTripOptionalImportedClass")
6945+
public func _bjs_roundTripOptionalImportedClass(_ vIsSome: Int32, _ vValue: Int32) -> Void {
6946+
#if arch(wasm32)
6947+
let ret = roundTripOptionalImportedClass(v: Optional<Foo>.bridgeJSLiftParameter(vIsSome, vValue))
6948+
return ret.bridgeJSLowerReturn()
6949+
#else
6950+
fatalError("Only available on WebAssembly")
6951+
#endif
6952+
}
6953+
69436954
@_expose(wasm, "bjs_throwsSwiftError")
69446955
@_cdecl("bjs_throwsSwiftError")
69456956
public func _bjs_throwsSwiftError(_ shouldThrow: Int32) -> Void {

Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11917,6 +11917,41 @@
1191711917
}
1191811918
}
1191911919
},
11920+
{
11921+
"abiName" : "bjs_roundTripOptionalImportedClass",
11922+
"effects" : {
11923+
"isAsync" : false,
11924+
"isStatic" : false,
11925+
"isThrows" : false
11926+
},
11927+
"name" : "roundTripOptionalImportedClass",
11928+
"parameters" : [
11929+
{
11930+
"label" : "v",
11931+
"name" : "v",
11932+
"type" : {
11933+
"nullable" : {
11934+
"_0" : {
11935+
"jsObject" : {
11936+
"_0" : "Foo"
11937+
}
11938+
},
11939+
"_1" : "null"
11940+
}
11941+
}
11942+
}
11943+
],
11944+
"returnType" : {
11945+
"nullable" : {
11946+
"_0" : {
11947+
"jsObject" : {
11948+
"_0" : "Foo"
11949+
}
11950+
},
11951+
"_1" : "null"
11952+
}
11953+
}
11954+
},
1192011955
{
1192111956
"abiName" : "bjs_throwsSwiftError",
1192211957
"effects" : {

0 commit comments

Comments
 (0)