BridgeJS: Export types using a separate JS representation#750
Conversation
kateinoigakukun
left a comment
There was a problem hiding this comment.
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
| @_cdecl("bjs_roundtripPolygon") | ||
| public func _bjs_roundtripPolygon(_ polygon: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { | ||
| #if arch(wasm32) | ||
| let ret = roundtripPolygon(_: Polygon.bridgeFromJS(PolygonReference.bridgeJSLiftParameter(polygon))) |
There was a problem hiding this comment.
Instead of calling bridgeFromJS / bridgeToJS in a call sequence directly, I think we can define Polygon.bridgeJSLiftParameter, then we don't need to add special handling in ExportedThunkBuilder and CallJSEmission, and also we can remove special conversions for container types (Dictionary, Array, Optional).
| case swiftProtocol(String) | ||
| case swiftStruct(String) | ||
| indirect case closure(ClosureSignature, useJSTypedClosure: Bool) | ||
| indirect case alias(name: String, underlying: BridgeType) |
There was a problem hiding this comment.
Do we really need to add alias case in BridgeType? If we generate conventional methods like bridgeJSLiftParameter and bridgeJSLowerParameter, we might be able to make BridgeType consumers agnostic to aliasness.
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 makingPolygona class), or a duplicate type could be created (say aJSPolygon) 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
Polygonin 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:
Now, the existing Swift code can continue to use the Swift-idiomatic
Polygonwhile JavaScript gets the appropriateJSPolygontype.Design
This is implemented by inserting calls to
bridgeToJS()andbridgeFromJSat 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 toBridgeTypeindirect case alias(name: String, underlying: BridgeType). I’m very much not sold on the namealias, but I couldn’t think of anything better.Alternatives
Polygoncase, it is also more general.