Skip to content

Commit 9706c4c

Browse files
authored
Merge pull request #754 from PassiveLogic/kr/fix-optional-jsclass-param
BridgeJS: Support optional @jsclass as exported function parameters
2 parents 388160c + 83098c2 commit 9706c4c

13 files changed

Lines changed: 320 additions & 28 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ struct IntrinsicJSFragment: Sendable {
669669
}
670670

671671
let innerFragment =
672-
if wrappedType.optionalConvention == .stackABI {
672+
if wrappedType.optionalParameterUsesStackABI {
673673
try stackLiftFragment(elementType: wrappedType)
674674
} else {
675675
try liftParameter(type: wrappedType, context: bridgeContext)
@@ -686,7 +686,7 @@ struct IntrinsicJSFragment: Sendable {
686686
kind: JSOptionalKind,
687687
innerFragment: IntrinsicJSFragment
688688
) -> IntrinsicJSFragment {
689-
let isStackConvention = wrappedType.optionalConvention == .stackABI
689+
let isStackConvention = wrappedType.optionalParameterUsesStackABI
690690
let absenceLiteral = kind.absenceLiteral
691691

692692
let outerParams: [String]
@@ -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"],
@@ -2699,6 +2699,24 @@ private extension BridgeType {
26992699
}
27002700
}
27012701

2702+
/// Whether an optional of this type pushes its payload onto the bridge stack
2703+
/// when passed as a *parameter*.
2704+
///
2705+
/// This usually matches `optionalConvention == .stackABI`, but `jsObject`
2706+
/// optionals are the exception: their return values travel through the stack
2707+
/// while their parameters use the direct `(isSome, objId)` ABI, matching plain
2708+
/// `Optional<JSObject>` and exported `@JS class` parameters.
2709+
var optionalParameterUsesStackABI: Bool {
2710+
switch self {
2711+
case .jsObject:
2712+
return false
2713+
case .nullable(let wrapped, _):
2714+
return wrapped.optionalParameterUsesStackABI
2715+
default:
2716+
return optionalConvention == .stackABI
2717+
}
2718+
}
2719+
27022720
var nilSentinel: NilSentinel {
27032721
switch self {
27042722
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: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -394,18 +394,9 @@ export async function createInstantiator(options, swift) {
394394
setException(error);
395395
}
396396
}
397-
TestModule["bjs_WithOptionalJSClass_childOrNull_set"] = function bjs_WithOptionalJSClass_childOrNull_set(self, newValue) {
397+
TestModule["bjs_WithOptionalJSClass_childOrNull_set"] = function bjs_WithOptionalJSClass_childOrNull_set(self, newValueIsSome, newValueObjectId) {
398398
try {
399-
let optResult;
400-
if (newValue) {
401-
const objId = i32Stack.pop();
402-
const obj = swift.memory.getObject(objId);
403-
swift.memory.release(objId);
404-
optResult = obj;
405-
} else {
406-
optResult = null;
407-
}
408-
swift.memory.getObject(self).childOrNull = optResult;
399+
swift.memory.getObject(self).childOrNull = newValueIsSome ? swift.memory.getObject(newValueObjectId) : null;
409400
} catch (error) {
410401
setException(error);
411402
}
@@ -496,22 +487,13 @@ export async function createInstantiator(options, swift) {
496487
setException(error);
497488
}
498489
}
499-
TestModule["bjs_WithOptionalJSClass_roundTripChildOrNull"] = function bjs_WithOptionalJSClass_roundTripChildOrNull(self, value) {
490+
TestModule["bjs_WithOptionalJSClass_roundTripChildOrNull"] = function bjs_WithOptionalJSClass_roundTripChildOrNull(self, valueIsSome, valueObjectId) {
500491
try {
501-
let optResult;
502-
if (value) {
503-
const objId = i32Stack.pop();
504-
const obj = swift.memory.getObject(objId);
505-
swift.memory.release(objId);
506-
optResult = obj;
507-
} else {
508-
optResult = null;
509-
}
510-
let ret = swift.memory.getObject(self).roundTripChildOrNull(optResult);
492+
let ret = swift.memory.getObject(self).roundTripChildOrNull(valueIsSome ? swift.memory.getObject(valueObjectId) : null);
511493
const isSome = ret != null;
512494
if (isSome) {
513-
const objId1 = swift.memory.retain(ret);
514-
i32Stack.push(objId1);
495+
const objId = swift.memory.retain(ret);
496+
i32Stack.push(objId);
515497
}
516498
i32Stack.push(isSome ? 1 : 0);
517499
} catch (error) {
@@ -732,6 +714,48 @@ export async function createInstantiator(options, swift) {
732714
const optResult = pointer === null ? null : OptionalPropertyHolder.__construct(pointer);
733715
return optResult;
734716
},
717+
roundTripExportedOptionalJSObject: function bjs_roundTripExportedOptionalJSObject(value) {
718+
const isSome = value != null;
719+
let result;
720+
if (isSome) {
721+
result = swift.memory.retain(value);
722+
} else {
723+
result = 0;
724+
}
725+
instance.exports.bjs_roundTripExportedOptionalJSObject(+isSome, result);
726+
const isSome1 = i32Stack.pop();
727+
let optResult;
728+
if (isSome1) {
729+
const objId = i32Stack.pop();
730+
const obj = swift.memory.getObject(objId);
731+
swift.memory.release(objId);
732+
optResult = obj;
733+
} else {
734+
optResult = null;
735+
}
736+
return optResult;
737+
},
738+
roundTripExportedOptionalJSClass: function bjs_roundTripExportedOptionalJSClass(value) {
739+
const isSome = value != null;
740+
let result;
741+
if (isSome) {
742+
result = swift.memory.retain(value);
743+
} else {
744+
result = 0;
745+
}
746+
instance.exports.bjs_roundTripExportedOptionalJSClass(+isSome, result);
747+
const isSome1 = i32Stack.pop();
748+
let optResult;
749+
if (isSome1) {
750+
const objId = i32Stack.pop();
751+
const obj = swift.memory.getObject(objId);
752+
swift.memory.release(objId);
753+
optResult = obj;
754+
} else {
755+
optResult = null;
756+
}
757+
return optResult;
758+
},
735759
roundTripString: function bjs_roundTripString(name) {
736760
const isSome = name != null;
737761
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6979,6 +6979,17 @@ public func _bjs_makeImportedFoo(_ valueBytes: Int32, _ valueLength: Int32) -> I
69796979
#endif
69806980
}
69816981

6982+
@_expose(wasm, "bjs_roundTripOptionalImportedClass")
6983+
@_cdecl("bjs_roundTripOptionalImportedClass")
6984+
public func _bjs_roundTripOptionalImportedClass(_ vIsSome: Int32, _ vValue: Int32) -> Void {
6985+
#if arch(wasm32)
6986+
let ret = roundTripOptionalImportedClass(v: Optional<Foo>.bridgeJSLiftParameter(vIsSome, vValue))
6987+
return ret.bridgeJSLowerReturn()
6988+
#else
6989+
fatalError("Only available on WebAssembly")
6990+
#endif
6991+
}
6992+
69826993
@_expose(wasm, "bjs_throwsSwiftError")
69836994
@_cdecl("bjs_throwsSwiftError")
69846995
public func _bjs_throwsSwiftError(_ shouldThrow: Int32) -> Void {
@@ -14895,6 +14906,18 @@ fileprivate func bjs_OptionalSupportImports_jsRoundTripOptionalStringToStringDic
1489514906
return bjs_OptionalSupportImports_jsRoundTripOptionalStringToStringDictionaryUndefined_static_extern(v)
1489614907
}
1489714908

14909+
#if arch(wasm32)
14910+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static")
14911+
fileprivate func bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static_extern(_ valueIsSome: Int32, _ valueValue: Int32) -> Void
14912+
#else
14913+
fileprivate func bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static_extern(_ valueIsSome: Int32, _ valueValue: Int32) -> Void {
14914+
fatalError("Only available on WebAssembly")
14915+
}
14916+
#endif
14917+
@inline(never) fileprivate func bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static(_ valueIsSome: Int32, _ valueValue: Int32) -> Void {
14918+
return bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static_extern(valueIsSome, valueValue)
14919+
}
14920+
1489814921
#if arch(wasm32)
1489914922
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_OptionalSupportImports_runJsOptionalSupportTests_static")
1490014923
fileprivate func bjs_OptionalSupportImports_runJsOptionalSupportTests_static_extern() -> Void
@@ -14981,6 +15004,15 @@ func _$OptionalSupportImports_jsRoundTripOptionalStringToStringDictionaryUndefin
1498115004
return JSUndefinedOr<[String: String]>.bridgeJSLiftReturn()
1498215005
}
1498315006

15007+
func _$OptionalSupportImports_jsRoundTripOptionalJSObjectNull(_ value: Optional<JSObject>) throws(JSException) -> Optional<JSObject> {
15008+
let (valueIsSome, valueValue) = value.bridgeJSLowerParameter()
15009+
bjs_OptionalSupportImports_jsRoundTripOptionalJSObjectNull_static(valueIsSome, valueValue)
15010+
if let error = _swift_js_take_exception() {
15011+
throw error
15012+
}
15013+
return Optional<JSObject>.bridgeJSLiftReturn()
15014+
}
15015+
1498415016
func _$OptionalSupportImports_runJsOptionalSupportTests() throws(JSException) -> Void {
1498515017
bjs_OptionalSupportImports_runJsOptionalSupportTests_static()
1498615018
if let error = _swift_js_take_exception() {

0 commit comments

Comments
 (0)