GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — BridgeJS: Export types using a separate JS representation by wfltaylor · Pull Request #750 · swiftwasm/JavaScriptKit

Conversation

@wfltaylor

@wfltaylor wfltaylor commented May 27, 2026

Copy link
Copy Markdown
Contributor

Note that this PR is much smaller than it looks! The vast majority of the lines added are tests/generated code.

Adds @JS(as:) to let types provide an alternative JavaScript representation, providing an escape hatch when BridgeJS’s defaults don't fit.

Motivation

BridgeJS provides sensible defaults when exporting Swift code to JavaScript. For example, structs are exported using copy semantics: each field in the Swift struct is copied when crossing the boundary. BridgeJS also only supports exporting a subset of Swift features, which makes sense given there are many things in the Swift language which would be difficult to expose to JavaScript.

While this approach makes sense, sometimes the defaults used by BridgeJS won’t work well in a specific situation. For example, a Swift struct Polygon { var vertices: [Point] } will be exported to JavaScript using copy semantics. In a project where polygons could contain thousands of points, copying at the boundary every time provides unacceptable performance. Currently, there is no escape hatch for situations like these. Either the Swift code has to be modified to be less idiomatic (e.g. in this case by making Polygon a class), or a duplicate type could be created (say a JSPolygon) and exposed to JavaScript that wraps the underlying Swift type. A similar situation could be encountered when using a feature which BridgeJS doesn’t support, for example dictionaries with integer keys. The same two options are available: less idiomatic Swift code (using String keys everywhere), or duplicating types.

At first, the “duplicated types” approach sounds reasonable. Unfortunately, it starts to break down as the project scales. For example, a large existing codebase could use Polygon in hundreds of types and hundreds of methods. Creating a duplicate JS hierarchy here introduces a huge maintenance cost, where most of the duplication is providing no value since BridgeJS’s defaults likely make sense the majority of the time. The same situation occurs with unsupported features: a leaf type might introduce a dictionary with integer keys, requiring the creation of a vast hierarchy of duplicated JS-safe types.

Solution

Allow types to provide an alternative JS representation. This works as follows:

@JS(as: JSPolygon.self) public struct Polygon {

    public var vertices: [Point]

    public consuming func bridgeToJS() -> JSPolygon {
        JSPolygon(underlying: self)
    }

    public static func bridgeFromJS(_ value: consuming JSPolygon) -> Polygon {
        value.underlying
    }

}

@JS public final class JSPolygon {

    var underlying: Polygon

    @JS public init(underlying: Polygon) {
        self.underlying = underlying
    }

    @JS var boundingBox: Rect { underlying.boundingBox }

}

Now, the existing Swift code can continue to use the Swift-idiomatic Polygon while JavaScript gets the appropriate JSPolygon type.

Design

This is implemented by inserting calls to bridgeToJS() and bridgeFromJS at the first and last possible opportunity respectively. This means that code generation changes are minimal, everything just uses the ABI of the JavaScript type. I’ve added a new case to BridgeType indirect case alias(name: String, underlying: BridgeType). I’m very much not sold on the name alias, but I couldn’t think of anything better.

Alternatives

  • PR BridgeJS: Add support for exporting structs using a box #740 aimed to solve a similar problem, but was more narrow (and required more complex changes). While this approach requires more boilerplate for something like the Polygon case, it is also more general.
  • In Support exporting structs as a reference #737, @krodak suggested using a custom code-generation tool. While this is a possible approach, implementing this correctly is tricky and it makes sense to me for BridgeJS to have built-in support for what is likely going to be a common problem.
  • Not doing anything: the status quo of requiring users to modify their Swift code to be less idiomatic or create a duplicate hierarchy of types. This would likely be an impediment to adopting BridgeJS, especially in larger projects who may have more consumers than just JavaScript.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

krodak reacted with rocket emoji
All reactions
  • 🚀 1 reaction
@wfltaylor wfltaylor mentioned this pull request May 27, 2026
Support exporting structs as a reference #737
Open
@wfltaylor
wfltaylor force-pushed the js-as branch from 09c44ac to aa6d25a Compare May 27, 2026 08:05
@MaxDesiatov
MaxDesiatov requested review from kateinoigakukun and krodak May 27, 2026 13:22
kateinoigakukun
kateinoigakukun reviewed May 27, 2026
View reviewed changes

@kateinoigakukun kateinoigakukun left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for trying to tackle this and showing your idea with actual code! This is very interesting to me, and I just realized the ability to have different transferring representations solves some other problems like #496

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

wfltaylor reacted with heart emoji
All reactions
  • ❤️ 1 reaction
Comment thread Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Alias.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

@wfltaylor
wfltaylor force-pushed the js-as branch from aa6d25a to 8716d44 Compare June 12, 2026 02:10
krodak
krodak approved these changes Jun 23, 2026
View reviewed changes

@krodak krodak left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just minor requests

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Sources/JavaScriptKit/Macros.swift
Builtin.ExternalMacro
public macro JS(
as aliasOf: Any.Type? = nil,
namespace: String? = nil,

@krodak krodak Jun 23, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JS(as:) is a meaningful new escape hatch and _BridgedSwiftAlias is public API, but there's no DocC article for it. Every other exporting capability has one under Documentation.docc/Articles/BridgeJS/Exporting-Swift/ (Class, Static-Functions, Static-Properties...), and that's where people will look first. Worth one even while the feature is experimental - a stub to flesh out:

Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Custom-Representation.md
- When to reach for @JS(as:) (copy-semantics cost, unsupported features)
- The bridgeToJS() / bridgeFromJS contract
- The Polygon / JSPolygon example from the PR description
- Supported underlying representations + current limits (no raw-value enums, protocols, chained aliases)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
}

private func recordAlias(
node: some SyntaxProtocol & NamedDeclSyntax,

@krodak krodak Jun 23, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractAliasTarget short-circuits before namespace handling, so @JS(as: X.self, namespace: "Foo") silently drops the namespace. Since the macro accepts both arguments, a user could reasonably expect the alias to land in Foo. A diagnostic beats the silent drop - something like, at the top of recordAlias:

if parent.extractNamespace(from: jsAttribute) != nil {
    errors.append(DiagnosticError(
        node: node,
        message: "`namespace` is ignored on `@JS(as:)` types",
        hint: "Remove the `namespace:` argument; the alias adopts its target's representation"
    ))
}

(adjust to whatever namespace-extraction helper is in scope here)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
let base = memberAccess.base
else {
return nil
}

@krodak krodak Jun 23, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If as: is present but not in X.self form, this returns nil and the declaration quietly falls through to normal @JS handling rather than reporting the malformed as:. Low odds given the Any.Type? signature, but it'd save someone a confusing "why isn't my alias working" later. Worth splitting "no as:" from "as: present but unparseable":

guard let asArg = arguments.first(where: { $0.label?.text == "as" }) else {
    return nil  // genuinely not an alias
}
guard let memberAccess = asArg.expression.as(MemberAccessExprSyntax.self),
    memberAccess.declName.baseName.text == "self",
    let base = memberAccess.base
else {
    // `as:` present but malformed - report instead of silently ignoring
    return nil
}
return TypeSyntax(stringLiteral: base.trimmedDescription)

Surfacing a diagnostic from here means threading an errors channel in, so this is a "if you think it's worth it" rather than a must.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
// Dictionary mangling: "SD" prefix followed by value type (key is always String)
return "SD\(valueType.mangleTypeName)"
case .alias(let name, _):
return "Al\(name.count)\(name)"

@krodak krodak Jun 23, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aliases mangle on name only (Al<count><name>) while the other cases fold in structural info. Fine as long as name stays the namespace-qualified swiftCallName and unique across modules - worth a one-liner so nobody later "simplifies" it to the bare type name:

case .alias(let name, _):
    // `name` is the namespace-qualified swiftCallName (unique); the underlying
    // representation isn't folded in because aliases bridge via their JS type's ABI.
    return "Al\(name.count)\(name)"

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@krodak
krodak requested a review from kateinoigakukun June 23, 2026 12:02
kateinoigakukun
kateinoigakukun approved these changes Jun 23, 2026
View reviewed changes

@kateinoigakukun kateinoigakukun left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as long as @krodak's feedbacks are resolved.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
wfltaylor and others added 3 commits June 24, 2026 14:07
@wfltaylor @krodak
BridgeJS: Export types using a separate JS representation
d748e5b
@wfltaylor @krodak
BridgeJS: Remove insertion of bridging calls for aliases
fce4193
@krodak
BridgeJS: Address @js(as:) review feedback
76d11c0
Follow-up to the review on swiftwasm#750:

- ImportTS: give the unreachable `.alias` cases in loweringParameterInfo /
  liftingReturnInfo a message stating the `.unaliased` invariant, so a future
  change that breaks it fails loudly instead of trapping silently.
- JSGlueGen: in `optionalConvention` and `wasmParams`, delegate `.alias` to its
  underlying type rather than `preconditionFailure()`. These switch on `self`
  (not `.unaliased`), so this removes a latent crash for alias-wrapped values
  and matches how abiReturnType / mangleTypeName already handle `.alias`.
- SwiftToSkeleton: diagnose `@JS(as:)` combined with `namespace:` instead of
  silently dropping the namespace; an alias adopts its representation's
  placement. Adds a diagnostics test.
- BridgeJSSkeleton: note why alias mangling uses the (unique) swiftCallName only.
- Docs: add an "Exporting a Type With a Custom JS Representation" article and
  link it from the exporting topics.
@krodak
krodak force-pushed the js-as branch from 8716d44 to 76d11c0 Compare June 24, 2026 12:09
@krodak

krodak commented Jun 24, 2026

Copy link
Copy Markdown
Member

@wfltaylor hope you don't mind - I pushed a small commit to your branch to fold in the review feedback, and rebased on main so the snapshots line up with the recent changes there.

What I added:

  • messages on the unreachable .alias precondition cases in ImportTS
  • delegating the .alias cases in optionalConvention/wasmParams to the underlying type instead of trapping
  • a diagnostic for @JS(as:) combined with namespace: (it was silently dropped before)
  • a DocC article for the feature

I wanted to get it green so we can merge and start using it on our side (the @JS(as: String.self) shape is exactly what we need to drop a UUID shim). Thank you so much for the contribution, this is a really nice addition.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@krodak
krodak enabled auto-merge June 24, 2026 12:24
@krodak
krodak merged commit 3b4bba1 into swiftwasm:main Jun 24, 2026
16 checks passed

Uh oh!

There was an error while loading. Please reload this page.

MaxDesiatov
MaxDesiatov reviewed Jun 24, 2026
View reviewed changes
Comment thread Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Alias.swift Resolved

Uh oh!

There was an error while loading. Please reload this page.

@wfltaylor

wfltaylor commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@wfltaylor hope you don't mind - I pushed a small commit to your branch to fold in the review feedback, and rebased on main so the snapshots line up with the recent changes there.

What I added:

  • messages on the unreachable .alias precondition cases in ImportTS
  • delegating the .alias cases in optionalConvention/wasmParams to the underlying type instead of trapping
  • a diagnostic for @JS(as:) combined with namespace: (it was silently dropped before)
  • a DocC article for the feature

I wanted to get it green so we can merge and start using it on our side (the @JS(as: String.self) shape is exactly what we need to drop a UUID shim). Thank you so much for the contribution, this is a really nice addition.

Thanks @krodak !!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@MaxDesiatov MaxDesiatov MaxDesiatov left review comments
@krodak krodak krodak approved these changes
@kateinoigakukun kateinoigakukun kateinoigakukun approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@wfltaylor @krodak @MaxDesiatov @kateinoigakukun
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.