Skip to content

BridgeJS: Enhance importing TS classes #371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 13, 2025
Merged
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
172 changes: 117 additions & 55 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,8 @@ struct BridgeJSLink {

func link() throws -> (outputJs: String, outputDts: String) {
var exportsLines: [String] = []
var importedLines: [String] = []
var classLines: [String] = []
var dtsExportLines: [String] = []
var dtsImportLines: [String] = []
var dtsClassLines: [String] = []

if exportedSkeletons.contains(where: { $0.classes.count > 0 }) {
Expand Down Expand Up @@ -84,57 +82,18 @@ struct BridgeJSLink {
}
}

var importObjectBuilders: [ImportObjectBuilder] = []
for skeletonSet in importedSkeletons {
importedLines.append("const \(skeletonSet.moduleName) = importObject[\"\(skeletonSet.moduleName)\"] = {};")
func assignToImportObject(name: String, function: [String]) {
var js = function
js[0] = "\(skeletonSet.moduleName)[\"\(name)\"] = " + js[0]
importedLines.append(contentsOf: js)
}
let importObjectBuilder = ImportObjectBuilder(moduleName: skeletonSet.moduleName)
for fileSkeleton in skeletonSet.children {
for function in fileSkeleton.functions {
let (js, dts) = try renderImportedFunction(function: function)
assignToImportObject(name: function.abiName(context: nil), function: js)
dtsImportLines.append(contentsOf: dts)
try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function)
}
for type in fileSkeleton.types {
for property in type.properties {
let getterAbiName = property.getterAbiName(context: type)
let (js, dts) = try renderImportedProperty(
property: property,
abiName: getterAbiName,
emitCall: { thunkBuilder in
thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type)
return try thunkBuilder.lowerReturnValue(returnType: property.type)
}
)
assignToImportObject(name: getterAbiName, function: js)
dtsImportLines.append(contentsOf: dts)

if !property.isReadonly {
let setterAbiName = property.setterAbiName(context: type)
let (js, dts) = try renderImportedProperty(
property: property,
abiName: setterAbiName,
emitCall: { thunkBuilder in
thunkBuilder.liftParameter(
param: Parameter(label: nil, name: "newValue", type: property.type)
)
thunkBuilder.callPropertySetter(name: property.name, returnType: property.type)
return nil
}
)
assignToImportObject(name: setterAbiName, function: js)
dtsImportLines.append(contentsOf: dts)
}
}
for method in type.methods {
let (js, dts) = try renderImportedMethod(context: type, method: method)
assignToImportObject(name: method.abiName(context: type), function: js)
dtsImportLines.append(contentsOf: dts)
}
try renderImportedType(importObjectBuilder: importObjectBuilder, type: type)
}
}
importObjectBuilders.append(importObjectBuilder)
}

let outputJs = """
Expand Down Expand Up @@ -175,7 +134,7 @@ struct BridgeJSLink {
target.set(tmpRetBytes);
tmpRetBytes = undefined;
}
\(importedLines.map { $0.indent(count: 12) }.joined(separator: "\n"))
\(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n"))
},
setInstance: (i) => {
instance = i;
Expand All @@ -198,7 +157,7 @@ struct BridgeJSLink {
dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) })
dtsLines.append("}")
dtsLines.append("export type Imports = {")
dtsLines.append(contentsOf: dtsImportLines.map { $0.indent(count: 4) })
dtsLines.append(contentsOf: importObjectBuilders.flatMap { $0.dtsImportLines }.map { $0.indent(count: 4) })
dtsLines.append("}")
let outputDts = """
// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit,
Expand Down Expand Up @@ -437,6 +396,11 @@ struct BridgeJSLink {
}
}

func callConstructor(name: String) {
let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))"
bodyLines.append("let ret = \(call);")
}

func callMethod(name: String, returnType: BridgeType) {
let call = "swift.memory.getObject(self).\(name)(\(parameterForwardings.joined(separator: ", ")))"
if returnType == .void {
Expand Down Expand Up @@ -475,7 +439,31 @@ struct BridgeJSLink {
}
}

func renderImportedFunction(function: ImportedFunctionSkeleton) throws -> (js: [String], dts: [String]) {
class ImportObjectBuilder {
var moduleName: String
var importedLines: [String] = []
var dtsImportLines: [String] = []

init(moduleName: String) {
self.moduleName = moduleName
importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = {};")
}

func assignToImportObject(name: String, function: [String]) {
var js = function
js[0] = "\(moduleName)[\"\(name)\"] = " + js[0]
importedLines.append(contentsOf: js)
}

func appendDts(_ lines: [String]) {
dtsImportLines.append(contentsOf: lines)
}
}

func renderImportedFunction(
importObjectBuilder: ImportObjectBuilder,
function: ImportedFunctionSkeleton
) throws {
let thunkBuilder = ImportedThunkBuilder()
for param in function.parameters {
thunkBuilder.liftParameter(param: param)
Expand All @@ -486,11 +474,85 @@ struct BridgeJSLink {
name: function.abiName(context: nil),
returnExpr: returnExpr
)
var dtsLines: [String] = []
dtsLines.append(
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
importObjectBuilder.appendDts(
[
"\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));"
]
)
return (funcLines, dtsLines)
importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines)
}

func renderImportedType(
importObjectBuilder: ImportObjectBuilder,
type: ImportedTypeSkeleton
) throws {
if let constructor = type.constructor {
try renderImportedConstructor(
importObjectBuilder: importObjectBuilder,
type: type,
constructor: constructor
)
}
for property in type.properties {
let getterAbiName = property.getterAbiName(context: type)
let (js, dts) = try renderImportedProperty(
property: property,
abiName: getterAbiName,
emitCall: { thunkBuilder in
thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type)
return try thunkBuilder.lowerReturnValue(returnType: property.type)
}
)
importObjectBuilder.assignToImportObject(name: getterAbiName, function: js)
importObjectBuilder.appendDts(dts)

if !property.isReadonly {
let setterAbiName = property.setterAbiName(context: type)
let (js, dts) = try renderImportedProperty(
property: property,
abiName: setterAbiName,
emitCall: { thunkBuilder in
thunkBuilder.liftParameter(
param: Parameter(label: nil, name: "newValue", type: property.type)
)
thunkBuilder.callPropertySetter(name: property.name, returnType: property.type)
return nil
}
)
importObjectBuilder.assignToImportObject(name: setterAbiName, function: js)
importObjectBuilder.appendDts(dts)
}
}
for method in type.methods {
let (js, dts) = try renderImportedMethod(context: type, method: method)
importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js)
importObjectBuilder.appendDts(dts)
}
}

func renderImportedConstructor(
importObjectBuilder: ImportObjectBuilder,
type: ImportedTypeSkeleton,
constructor: ImportedConstructorSkeleton
) throws {
let thunkBuilder = ImportedThunkBuilder()
for param in constructor.parameters {
thunkBuilder.liftParameter(param: param)
}
let returnType = BridgeType.jsObject(type.name)
thunkBuilder.callConstructor(name: type.name)
let returnExpr = try thunkBuilder.lowerReturnValue(returnType: returnType)
let abiName = constructor.abiName(context: type)
let funcLines = thunkBuilder.renderFunction(
name: abiName,
returnExpr: returnExpr
)
importObjectBuilder.assignToImportObject(name: abiName, function: funcLines)
importObjectBuilder.appendDts([
"\(type.name): {",
"new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4),
"}",
])
}

func renderImportedProperty(
Expand Down Expand Up @@ -552,8 +614,8 @@ extension BridgeType {
return "number"
case .bool:
return "boolean"
case .jsObject:
return "any"
case .jsObject(let name):
return name ?? "any"
case .swiftHeapObject(let name):
return name
}
Expand Down
2 changes: 1 addition & 1 deletion Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ struct ImportTS {
preconditionFailure("assignThis can only be called with a jsObject return type")
}
abiReturnType = .i32
body.append("self.this = ret")
body.append("self.this = JSObject(id: UInt32(bitPattern: ret))")
}

func renderImportDecl() -> DeclSyntax {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
export type Exports = {
}
export type Imports = {
returnAnimatable(): any;
returnAnimatable(): Animatable;
}
export function createInstantiator(options: {
imports: Imports;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
export type Exports = {
}
export type Imports = {
Greeter: {
new(name: string): Greeter;
}
}
export function createInstantiator(options: {
imports: Imports;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export async function createInstantiator(options, swift) {
tmpRetBytes = undefined;
}
const TestModule = importObject["TestModule"] = {};
TestModule["bjs_Greeter_init"] = function bjs_Greeter_init(name) {
const nameObject = swift.memory.getObject(name);
swift.memory.release(name);
let ret = new options.imports.Greeter(nameObject);
return swift.memory.retain(ret);
}
TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) {
let ret = swift.memory.getObject(self).greet();
tmpRetBytes = textEncoder.encode(ret);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct Greeter {
_make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
}
let ret = bjs_Greeter_init(nameId)
self.this = ret
self.this = JSObject(id: UInt32(bitPattern: ret))
}

func greet() -> String {
Expand Down
2 changes: 1 addition & 1 deletion Plugins/PackageToJS/Templates/bin/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const harnesses = {
if (preludeScript) {
const prelude = await import(preludeScript)
if (prelude.setupOptions) {
options = prelude.setupOptions(options, { isMainThread: true })
options = await prelude.setupOptions(options, { isMainThread: true })
}
}
process.on("beforeExit", () => {
Expand Down
2 changes: 1 addition & 1 deletion Plugins/PackageToJS/Templates/platforms/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function createDefaultWorkerFactory(preludeScript) {
if (preludeScript) {
const prelude = await import(preludeScript);
if (prelude.setupOptions) {
options = prelude.setupOptions(options, { isMainThread: false })
options = await prelude.setupOptions(options, { isMainThread: false })
}
}
await instantiateForThread(tid, startArg, {
Expand Down
7 changes: 7 additions & 0 deletions Plugins/PackageToJS/Templates/test.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { InstantiateOptions, instantiate } from "./instantiate";

export type SetupOptionsFn = (
options: InstantiateOptions,
context: {
isMainThread: boolean,
}
) => Promise<InstantiateOptions>

export function testBrowser(
options: {
preludeScript?: string,
Expand Down
1 change: 1 addition & 0 deletions Plugins/PackageToJS/Templates/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export async function testBrowserInPage(options, processInfo) {
});

const { instantiate } = await import("./instantiate.js");
/** @type {import('./test.d.ts').SetupOptionsFn} */
let setupOptions = (options, _) => { return options };
if (processInfo.preludeScript) {
const prelude = await import(processInfo.preludeScript);
Expand Down
44 changes: 44 additions & 0 deletions Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,48 @@ func jsRoundTripString(_ v: String) -> String {
_init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
return Int(ret)
}
}

struct JsGreeter {
let this: JSObject

init(this: JSObject) {
self.this = this
}

init(takingThis this: Int32) {
self.this = JSObject(id: UInt32(bitPattern: this))
}

init(_ name: String) {
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init")
func bjs_JsGreeter_init(_ name: Int32) -> Int32
var name = name
let nameId = name.withUTF8 { b in
_make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
}
let ret = bjs_JsGreeter_init(nameId)
self.this = JSObject(id: UInt32(bitPattern: ret))
}

func greet() -> String {
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet")
func bjs_JsGreeter_greet(_ self: Int32) -> Int32
let ret = bjs_JsGreeter_greet(Int32(bitPattern: self.this.id))
return String(unsafeUninitializedCapacity: Int(ret)) { b in
_init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
return Int(ret)
}
}

func changeName(_ name: String) -> Void {
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_changeName")
func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void
var name = name
let nameId = name.withUTF8 { b in
_make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
}
bjs_JsGreeter_changeName(Int32(bitPattern: self.this.id), nameId)
}

}
Loading