Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,11 @@ extension BridgeType {
switch self {
case .swiftStruct, .array, .dictionary, .associatedValueEnum:
return true
case .nullable(.jsObject, _):
// Optional jsObject (incl. @JSClass) parameters travel via the bridge
// stack (isSome + object id), unlike their non-optional counterparts
// which are passed as a direct i32 parameter.
return true
case .nullable(let wrapped, _):
return wrapped.isStackUsingParameter
default:
Expand Down Expand Up @@ -1531,6 +1536,12 @@ extension BridgeType {
case .unsafePointer: return .unsafePointer
case .swiftProtocol: return .jsObject
case .void: return .void
case .nullable(.jsObject, _):
// Optional jsObject (incl. @JSClass) uses the stack ABI: the presence
// flag and object id are transferred through the bridge stack rather
// than as direct ABI parameters. The lift expression is the no-argument
// `Optional<T>.bridgeJSLiftParameter()` that pops them off the stack.
return LiftingIntrinsicInfo(parameters: [])
case .nullable(let wrappedType, _):
let wrappedInfo = try wrappedType.liftParameterInfo()
if wrappedInfo.parameters.isEmpty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class OptionalPropertyHolder {

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

// Exported functions taking/returning an optional jsObject use the stack ABI.
@JS func roundTripExportedOptionalJSObject(value: JSObject?) -> JSObject?

// Exported function taking/returning an optional imported @JSClass (issue #751).
@JS func roundTripExportedOptionalJSClass(value: WithOptionalJSClass?) -> WithOptionalJSClass?

@JS
func roundTripString(name: String?) -> String? {
return name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,76 @@
}
}
},
{
"abiName" : "bjs_roundTripExportedOptionalJSObject",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"name" : "roundTripExportedOptionalJSObject",
"parameters" : [
{
"label" : "value",
"name" : "value",
"type" : {
"nullable" : {
"_0" : {
"jsObject" : {

}
},
"_1" : "null"
}
}
}
],
"returnType" : {
"nullable" : {
"_0" : {
"jsObject" : {

}
},
"_1" : "null"
}
}
},
{
"abiName" : "bjs_roundTripExportedOptionalJSClass",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"name" : "roundTripExportedOptionalJSClass",
"parameters" : [
{
"label" : "value",
"name" : "value",
"type" : {
"nullable" : {
"_0" : {
"jsObject" : {
"_0" : "WithOptionalJSClass"
}
},
"_1" : "null"
}
}
}
],
"returnType" : {
"nullable" : {
"_0" : {
"jsObject" : {
"_0" : "WithOptionalJSClass"
}
},
"_1" : "null"
}
}
},
{
"abiName" : "bjs_roundTripString",
"effects" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ public func _bjs_testOptionalPropertyRoundtrip(_ holderIsSome: Int32, _ holderVa
#endif
}

@_expose(wasm, "bjs_roundTripExportedOptionalJSObject")
@_cdecl("bjs_roundTripExportedOptionalJSObject")
public func _bjs_roundTripExportedOptionalJSObject() -> Void {
#if arch(wasm32)
let ret = roundTripExportedOptionalJSObject(value: Optional<JSObject>.bridgeJSLiftParameter())
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_roundTripExportedOptionalJSClass")
@_cdecl("bjs_roundTripExportedOptionalJSClass")
public func _bjs_roundTripExportedOptionalJSClass() -> Void {
#if arch(wasm32)
let ret = roundTripExportedOptionalJSClass(value: Optional<WithOptionalJSClass>.bridgeJSLiftParameter())
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_roundTripString")
@_cdecl("bjs_roundTripString")
public func _bjs_roundTripString(_ nameIsSome: Int32, _ nameBytes: Int32, _ nameLength: Int32) -> Void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type Exports = {
}
roundTripOptionalClass(value: Greeter | null): Greeter | null;
testOptionalPropertyRoundtrip(holder: OptionalPropertyHolder | null): OptionalPropertyHolder | null;
roundTripExportedOptionalJSObject(value: any | null): any | null;
roundTripExportedOptionalJSClass(value: WithOptionalJSClass | null): WithOptionalJSClass | null;
roundTripString(name: string | null): string | null;
roundTripInt(value: number | null): number | null;
roundTripInt8(value: number | null): number | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,46 @@ export async function createInstantiator(options, swift) {
const optResult = pointer === null ? null : OptionalPropertyHolder.__construct(pointer);
return optResult;
},
roundTripExportedOptionalJSObject: function bjs_roundTripExportedOptionalJSObject(value) {
const isSome = value != null;
if (isSome) {
const objId = swift.memory.retain(value);
i32Stack.push(objId);
}
i32Stack.push(+isSome);
instance.exports.bjs_roundTripExportedOptionalJSObject();
const isSome1 = i32Stack.pop();
let optResult;
if (isSome1) {
const objId1 = i32Stack.pop();
const obj = swift.memory.getObject(objId1);
swift.memory.release(objId1);
optResult = obj;
} else {
optResult = null;
}
return optResult;
},
roundTripExportedOptionalJSClass: function bjs_roundTripExportedOptionalJSClass(value) {
const isSome = value != null;
if (isSome) {
const objId = swift.memory.retain(value);
i32Stack.push(objId);
}
i32Stack.push(+isSome);
instance.exports.bjs_roundTripExportedOptionalJSClass();
const isSome1 = i32Stack.pop();
let optResult;
if (isSome1) {
const objId1 = i32Stack.pop();
const obj = swift.memory.getObject(objId1);
swift.memory.release(objId1);
optResult = obj;
} else {
optResult = null;
}
return optResult;
},
roundTripString: function bjs_roundTripString(name) {
const isSome = name != null;
let result, result1;
Expand Down
13 changes: 13 additions & 0 deletions Sources/JavaScriptKit/BridgeJSIntrinsics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,19 @@ extension _BridgedAsOptional where Wrapped == JSObject {
}
}

extension _BridgedAsOptional where Wrapped: _JSBridgedClass {
// `@JSClass` wrappers (`_JSBridgedClass`) bridge an underlying `JSObject`, so an
// optional wrapper uses the same stack ABI as `Optional<JSObject>`: the presence
// flag and retained object id are transferred through the bridge stack.
//
// Lifting (`bridgeJSLiftParameter()` / `bridgeJSLiftReturn()`) and stack push/pop
// are already provided by the generic `Wrapped: _BridgedSwiftStackType` extension;
// only the export return path needs a dedicated lowering here.
@_spi(BridgeJS) public consuming func bridgeJSLowerReturn() -> Void {
Wrapped.bridgeJSStackPushAsOptional(asOptional)
}
}

extension _BridgedAsOptional where Wrapped: _BridgedSwiftProtocolWrapper {
@_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32, _ objectId: Int32) -> Self {
Self(
Expand Down
4 changes: 4 additions & 0 deletions Tests/BridgeJSRuntimeTests/ExportAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ func runJsWorks() -> Void
return try Foo(value)
}

@JS func roundTripOptionalImportedClass(v: Foo?) -> Foo? {
return v
}

struct TestError: Error {
let message: String
}
Expand Down
15 changes: 13 additions & 2 deletions Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5262,9 +5262,9 @@ public func _bjs_OptionalSupportExports_static_roundTripOptionalAPIOptionalResul

@_expose(wasm, "bjs_OptionalSupportExports_static_takeOptionalJSObject")
@_cdecl("bjs_OptionalSupportExports_static_takeOptionalJSObject")
public func _bjs_OptionalSupportExports_static_takeOptionalJSObject(_ valueIsSome: Int32, _ valueValue: Int32) -> Void {
public func _bjs_OptionalSupportExports_static_takeOptionalJSObject() -> Void {
#if arch(wasm32)
OptionalSupportExports.takeOptionalJSObject(_: Optional<JSObject>.bridgeJSLiftParameter(valueIsSome, valueValue))
OptionalSupportExports.takeOptionalJSObject(_: Optional<JSObject>.bridgeJSLiftParameter())
#else
fatalError("Only available on WebAssembly")
#endif
Expand Down Expand Up @@ -6940,6 +6940,17 @@ public func _bjs_makeImportedFoo(_ valueBytes: Int32, _ valueLength: Int32) -> I
#endif
}

@_expose(wasm, "bjs_roundTripOptionalImportedClass")
@_cdecl("bjs_roundTripOptionalImportedClass")
public func _bjs_roundTripOptionalImportedClass() -> Void {
#if arch(wasm32)
let ret = roundTripOptionalImportedClass(v: Optional<Foo>.bridgeJSLiftParameter())
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_throwsSwiftError")
@_cdecl("bjs_throwsSwiftError")
public func _bjs_throwsSwiftError(_ shouldThrow: Int32) -> Void {
Expand Down
35 changes: 35 additions & 0 deletions Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json
Original file line number Diff line number Diff line change
Expand Up @@ -11917,6 +11917,41 @@
}
}
},
{
"abiName" : "bjs_roundTripOptionalImportedClass",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"name" : "roundTripOptionalImportedClass",
"parameters" : [
{
"label" : "v",
"name" : "v",
"type" : {
"nullable" : {
"_0" : {
"jsObject" : {
"_0" : "Foo"
}
},
"_1" : "null"
}
}
}
],
"returnType" : {
"nullable" : {
"_0" : {
"jsObject" : {
"_0" : "Foo"
}
},
"_1" : "null"
}
}
},
{
"abiName" : "bjs_throwsSwiftError",
"effects" : {
Expand Down
7 changes: 7 additions & 0 deletions Tests/prelude.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,13 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) {
assert.ok(foo instanceof ImportedFoo);
assert.equal(foo.value, "hello");

// Optional @JSClass directly as an exported function parameter/return value (issue #751)
const optFoo = new ImportedFoo("optional-foo");
const optFooResult = exports.roundTripOptionalImportedClass(optFoo);
assert.ok(optFooResult instanceof ImportedFoo);
assert.equal(optFooResult.value, "optional-foo");
assert.equal(exports.roundTripOptionalImportedClass(null), null);

// Test PropertyHolder with various types
const testObj = { testProp: "test" };
const sibling = new exports.SimplePropertyHolder(999);
Expand Down
Loading