Skip to content

Commit 36a06c5

Browse files
Implement JSString to reduce bridging overhead (#63)
* Implement JSString to reduce briding overhead * Bump runtime library version
1 parent 702dd89 commit 36a06c5

File tree

11 files changed

+194
-41
lines changed

11 files changed

+194
-41
lines changed

IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #li
7979

8080
func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
8181
switch value {
82-
case let .string(string): return string
82+
case let .string(string): return String(string)
8383
default:
8484
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
8585
}

IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import JavaScriptKit
22

3+
34
try test("Literal Conversion") {
45
let global = JSObject.global
56
let inputs: [JSValue] = [
@@ -16,7 +17,7 @@ try test("Literal Conversion") {
1617
.undefined,
1718
]
1819
for (index, input) in inputs.enumerated() {
19-
let prop = "prop_\(index)"
20+
let prop = JSString("prop_\(index)")
2021
setJSValue(this: global, name: prop, value: input)
2122
let got = getJSValue(this: global, name: prop)
2223
switch (got, input) {

Runtime/src/index.ts

+22-12
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class SwiftRuntimeHeap {
117117
export class SwiftRuntime {
118118
private instance: WebAssembly.Instance | null;
119119
private heap: SwiftRuntimeHeap
120-
private version: number = 610
120+
private version: number = 611
121121

122122
constructor() {
123123
this.instance = null;
@@ -163,9 +163,8 @@ export class SwiftRuntime {
163163
const textDecoder = new TextDecoder('utf-8');
164164
const textEncoder = new TextEncoder(); // Only support utf-8
165165

166-
const readString = (ptr: pointer, len: number) => {
167-
const uint8Memory = new Uint8Array(memory().buffer);
168-
return textDecoder.decode(uint8Memory.subarray(ptr, ptr + len));
166+
const readString = (ref: ref) => {
167+
return this.heap.referenceHeap(ref);
169168
}
170169

171170
const writeString = (ptr: pointer, bytes: Uint8Array) => {
@@ -217,7 +216,7 @@ export class SwiftRuntime {
217216
return payload3;
218217
}
219218
case JavaScriptValueKind.String: {
220-
return readString(payload1, payload2)
219+
return readString(payload1);
221220
}
222221
case JavaScriptValueKind.Object: {
223222
return this.heap.referenceHeap(payload1)
@@ -261,10 +260,9 @@ export class SwiftRuntime {
261260
break;
262261
}
263262
case "string": {
264-
const bytes = textEncoder.encode(value);
265263
writeUint32(kind_ptr, JavaScriptValueKind.String);
266-
writeUint32(payload1_ptr, this.heap.retain(bytes));
267-
writeUint32(payload2_ptr, bytes.length);
264+
writeUint32(payload1_ptr, this.heap.retain(value));
265+
writeUint32(payload2_ptr, 0);
268266
break;
269267
}
270268
case "undefined": {
@@ -308,20 +306,20 @@ export class SwiftRuntime {
308306

309307
return {
310308
swjs_set_prop: (
311-
ref: ref, name: pointer, length: number,
309+
ref: ref, name: ref,
312310
kind: JavaScriptValueKind,
313311
payload1: number, payload2: number, payload3: number
314312
) => {
315313
const obj = this.heap.referenceHeap(ref);
316-
Reflect.set(obj, readString(name, length), decodeValue(kind, payload1, payload2, payload3))
314+
Reflect.set(obj, readString(name), decodeValue(kind, payload1, payload2, payload3))
317315
},
318316
swjs_get_prop: (
319-
ref: ref, name: pointer, length: number,
317+
ref: ref, name: ref,
320318
kind_ptr: pointer,
321319
payload1_ptr: pointer, payload2_ptr: pointer, payload3_ptr: number
322320
) => {
323321
const obj = this.heap.referenceHeap(ref);
324-
const result = Reflect.get(obj, readString(name, length));
322+
const result = Reflect.get(obj, readString(name));
325323
writeValue(result, kind_ptr, payload1_ptr, payload2_ptr, payload3_ptr);
326324
},
327325
swjs_set_subscript: (
@@ -341,6 +339,18 @@ export class SwiftRuntime {
341339
const result = Reflect.get(obj, index);
342340
writeValue(result, kind_ptr, payload1_ptr, payload2_ptr, payload3_ptr);
343341
},
342+
swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => {
343+
const bytes = textEncoder.encode(this.heap.referenceHeap(ref));
344+
const bytes_ptr = this.heap.retain(bytes);
345+
writeUint32(bytes_ptr_result, bytes_ptr);
346+
return bytes.length;
347+
},
348+
swjs_decode_string: (bytes_ptr: pointer, length: number) => {
349+
const uint8Memory = new Uint8Array(memory().buffer);
350+
const bytes = uint8Memory.subarray(bytes_ptr, bytes_ptr + length);
351+
const string = textDecoder.decode(bytes);
352+
return this.heap.retain(string);
353+
},
344354
swjs_load_string: (ref: ref, buffer: pointer) => {
345355
const bytes = this.heap.referenceHeap(ref);
346356
writeString(buffer, bytes);

Sources/JavaScriptKit/Compatibility.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`.
44
@_cdecl("swjs_library_version")
55
func _library_version() -> Double {
6-
return 610
6+
return 611
77
}

Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

+8
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ public class JSObject: Equatable {
5757
/// - Parameter name: The name of this object's member to access.
5858
/// - Returns: The value of the `name` member of this object.
5959
public subscript(_ name: String) -> JSValue {
60+
get { getJSValue(this: self, name: JSString(name)) }
61+
set { setJSValue(this: self, name: JSString(name), value: newValue) }
62+
}
63+
64+
/// Access the `name` member dynamically through JavaScript and Swift runtime bridge library.
65+
/// - Parameter name: The name of this object's member to access.
66+
/// - Returns: The value of the `name` member of this object.
67+
public subscript(_ name: JSString) -> JSValue {
6068
get { getJSValue(this: self, name: name) }
6169
set { setJSValue(this: self, name: name, value: newValue) }
6270
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import _CJavaScriptKit
2+
3+
/// `JSString` represents a string in JavaScript and supports bridging string between JavaScript and Swift.
4+
///
5+
/// Conversion between `Swift.String` and `JSString` can be:
6+
///
7+
/// ```swift
8+
/// // Convert `Swift.String` to `JSString`
9+
/// let jsString: JSString = ...
10+
/// let swiftString: String = String(jsString)
11+
///
12+
/// // Convert `JSString` to `Swift.String`
13+
/// let swiftString: String = ...
14+
/// let jsString: JSString = JSString(swiftString)
15+
/// ```
16+
///
17+
public struct JSString: LosslessStringConvertible, Equatable {
18+
/// The internal representation of JS compatible string
19+
/// The initializers of this type must initialize `jsRef` or `buffer`.
20+
/// And the uninitialized one will be lazily initialized
21+
class Guts {
22+
var shouldDealocateRef: Bool = false
23+
lazy var jsRef: JavaScriptObjectRef = {
24+
self.shouldDealocateRef = true
25+
return buffer.withUTF8 { bufferPtr in
26+
return _decode_string(bufferPtr.baseAddress!, Int32(bufferPtr.count))
27+
}
28+
}()
29+
30+
lazy var buffer: String = {
31+
var bytesRef: JavaScriptObjectRef = 0
32+
let bytesLength = Int(_encode_string(jsRef, &bytesRef))
33+
// +1 for null terminator
34+
let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self)
35+
defer {
36+
free(buffer)
37+
_release(bytesRef)
38+
}
39+
_load_string(bytesRef, buffer)
40+
buffer[bytesLength] = 0
41+
return String(decodingCString: UnsafePointer(buffer), as: UTF8.self)
42+
}()
43+
44+
init(from stringValue: String) {
45+
self.buffer = stringValue
46+
}
47+
48+
init(from jsRef: JavaScriptObjectRef) {
49+
self.jsRef = jsRef
50+
self.shouldDealocateRef = true
51+
}
52+
53+
deinit {
54+
guard shouldDealocateRef else { return }
55+
_release(jsRef)
56+
}
57+
}
58+
59+
let guts: Guts
60+
61+
internal init(jsRef: JavaScriptObjectRef) {
62+
self.guts = Guts(from: jsRef)
63+
}
64+
65+
/// Instantiate a new `JSString` with given Swift.String.
66+
public init(_ stringValue: String) {
67+
self.guts = Guts(from: stringValue)
68+
}
69+
70+
/// A Swift representation of this `JSString`.
71+
/// Note that this accessor may copy the JS string value into Swift side memory.
72+
public var description: String { guts.buffer }
73+
74+
/// Returns a Boolean value indicating whether two strings are equal values.
75+
///
76+
/// - Parameters:
77+
/// - lhs: A string to compare.
78+
/// - rhs: Another string to compare.
79+
public static func == (lhs: JSString, rhs: JSString) -> Bool {
80+
return lhs.guts.buffer == rhs.guts.buffer
81+
}
82+
}
83+
84+
extension JSString: ExpressibleByStringLiteral {
85+
public init(stringLiteral value: String) {
86+
self.init(value)
87+
}
88+
}
89+
90+
91+
// MARK: - Internal Helpers
92+
extension JSString {
93+
94+
func asInternalJSRef() -> JavaScriptObjectRef {
95+
guts.jsRef
96+
}
97+
98+
func withRawJSValue<T>(_ body: (RawJSValue) -> T) -> T {
99+
let rawValue = RawJSValue(
100+
kind: .string, payload1: guts.jsRef, payload2: 0, payload3: 0
101+
)
102+
return body(rawValue)
103+
}
104+
}

Sources/JavaScriptKit/JSValue.swift

+22-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import _CJavaScriptKit
33
/// `JSValue` represents a value in JavaScript.
44
public enum JSValue: Equatable {
55
case boolean(Bool)
6-
case string(String)
6+
case string(JSString)
77
case number(Double)
88
case object(JSObject)
99
case null
@@ -21,7 +21,18 @@ public enum JSValue: Equatable {
2121

2222
/// Returns the `String` value of this JS value if the type is string.
2323
/// If not, returns `nil`.
24+
///
25+
/// Note that this accessor may copy the JS string value into Swift side memory.
26+
///
27+
/// To avoid the copying, please consider the `jsString` instead.
2428
public var string: String? {
29+
jsString.map(String.init)
30+
}
31+
32+
/// Returns the `JSString` value of this JS value if the type is string.
33+
/// If not, returns `nil`.
34+
///
35+
public var jsString: JSString? {
2536
switch self {
2637
case let .string(string): return string
2738
default: return nil
@@ -76,6 +87,10 @@ extension JSValue {
7687

7788
extension JSValue {
7889

90+
public static func string(_ value: String) -> JSValue {
91+
.string(JSString(value))
92+
}
93+
7994
/// Deprecated: Please create `JSClosure` directly and manage its lifetime manually.
8095
///
8196
/// Migrate this usage
@@ -108,7 +123,7 @@ extension JSValue {
108123

109124
extension JSValue: ExpressibleByStringLiteral {
110125
public init(stringLiteral value: String) {
111-
self = .string(value)
126+
self = .string(JSString(value))
112127
}
113128
}
114129

@@ -130,17 +145,17 @@ extension JSValue: ExpressibleByNilLiteral {
130145
}
131146
}
132147

133-
public func getJSValue(this: JSObject, name: String) -> JSValue {
148+
public func getJSValue(this: JSObject, name: JSString) -> JSValue {
134149
var rawValue = RawJSValue()
135-
_get_prop(this.id, name, Int32(name.count),
150+
_get_prop(this.id, name.asInternalJSRef(),
136151
&rawValue.kind,
137152
&rawValue.payload1, &rawValue.payload2, &rawValue.payload3)
138153
return rawValue.jsValue()
139154
}
140155

141-
public func setJSValue(this: JSObject, name: String, value: JSValue) {
156+
public func setJSValue(this: JSObject, name: JSString, value: JSValue) {
142157
value.withRawJSValue { rawValue in
143-
_set_prop(this.id, name, Int32(name.count), rawValue.kind, rawValue.payload1, rawValue.payload2, rawValue.payload3)
158+
_set_prop(this.id, name.asInternalJSRef(), rawValue.kind, rawValue.payload1, rawValue.payload2, rawValue.payload3)
144159
}
145160
}
146161

@@ -183,7 +198,7 @@ extension JSValue: CustomStringConvertible {
183198
case let .boolean(boolean):
184199
return boolean.description
185200
case .string(let string):
186-
return string
201+
return string.description
187202
case .number(let number):
188203
return number.description
189204
case .object(let object), .function(let object as JSObject):

Sources/JavaScriptKit/JSValueConstructible.swift

+6
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,9 @@ extension UInt64: JSValueConstructible {
9191
value.number.map(Self.init)
9292
}
9393
}
94+
95+
extension JSString: JSValueConstructible {
96+
public static func construct(from value: JSValue) -> JSString? {
97+
value.jsString
98+
}
99+
}

Sources/JavaScriptKit/JSValueConvertible.swift

+8-15
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension Double: JSValueConvertible {
3737
}
3838

3939
extension String: JSValueConvertible {
40-
public func jsValue() -> JSValue { .string(self) }
40+
public func jsValue() -> JSValue { .string(JSString(self)) }
4141
}
4242

4343
extension UInt8: JSValueConvertible {
@@ -72,6 +72,10 @@ extension Int64: JSValueConvertible {
7272
public func jsValue() -> JSValue { .number(Double(self)) }
7373
}
7474

75+
extension JSString: JSValueConvertible {
76+
public func jsValue() -> JSValue { .string(self) }
77+
}
78+
7579
extension JSObject: JSValueCodable {
7680
// `JSObject.jsValue` is defined in JSObject.swift to be able to overridden
7781
// from `JSFunction`
@@ -181,13 +185,7 @@ extension RawJSValue: JSValueConvertible {
181185
case .number:
182186
return .number(payload3)
183187
case .string:
184-
// +1 for null terminator
185-
let buffer = malloc(Int(payload2 + 1))!.assumingMemoryBound(to: UInt8.self)
186-
defer { free(buffer) }
187-
_load_string(JavaScriptObjectRef(payload1), buffer)
188-
buffer[Int(payload2)] = 0
189-
let string = String(decodingCString: UnsafePointer(buffer), as: UTF8.self)
190-
return .string(string)
188+
return .string(JSString(jsRef: payload1))
191189
case .object:
192190
return .object(JSObject(id: UInt32(payload1)))
193191
case .null:
@@ -218,13 +216,8 @@ extension JSValue {
218216
payload1 = 0
219217
payload2 = 0
220218
payload3 = numberValue
221-
case var .string(stringValue):
222-
kind = .string
223-
return stringValue.withUTF8 { bufferPtr in
224-
let ptrValue = UInt32(UInt(bitPattern: bufferPtr.baseAddress!))
225-
let rawValue = RawJSValue(kind: kind, payload1: JavaScriptPayload1(ptrValue), payload2: JavaScriptPayload2(bufferPtr.count), payload3: 0)
226-
return body(rawValue)
227-
}
219+
case let .string(string):
220+
return string.withRawJSValue(body)
228221
case let .object(ref):
229222
kind = .object
230223
payload1 = JavaScriptPayload1(ref.id)

0 commit comments

Comments
 (0)