@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.107 → 0.1.1-beta.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -171,18 +171,38 @@ fi
171
171
  # A plugin's class is a Swift type name, which the registry creates, and its
172
172
  # sources are Swift.
173
173
  swift_type='^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$'
174
+ # Swift requires unique file names in a module, so the source staged as
175
+ # <plugin>/<key>/<name> compiles as <plugin>-<key>-<name>.
176
+ plugin_sources=$temporary/plugin-sources
177
+ mkdir "$plugin_sources"
178
+ add_plugin_source() {
179
+ local plugin_id=$1 source=$2
180
+ local key=${source%/*}
181
+ local named=$plugin_sources/$plugin_id-${key##*/}-${source##*/}
182
+ cp "$source" "$named"
183
+ swift_sources+=("$named")
184
+ }
174
185
  registry=$temporary/TokamakPluginRegistry.swift
175
186
  printf '%s\n\n%s\n%s\n' 'import Foundation' 'func tokamakPlugins(app: TokamakApp) -> [any TokamakPlugin] {' ' [' > "$registry"
176
187
  for plugin in "$input/plugins"/*/; do
188
+ plugin_id=$(basename "$plugin")
177
189
  class=$(cat "$plugin/class" 2>/dev/null || true)
178
- if [[ ! "$class" =~ $swift_type ]]; then
179
- echo "plugin '$(basename "$plugin")' has invalid $platform class '$class'" >&2
180
- exit 1
190
+ # A plugin whose native code is its generated bindings alone has no class.
191
+ if [[ -n "$class" || ! -d "$plugin/bindings" ]]; then
192
+ if [[ ! "$class" =~ $swift_type ]]; then
193
+ echo "plugin '$plugin_id' has invalid $platform class '$class'" >&2
194
+ exit 1
195
+ fi
196
+ printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$plugin_id" >> "$registry"
181
197
  fi
182
- printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$(basename "$plugin")" >> "$registry"
198
+ # `tok bindings` names the generated class TokamakBindings_<id>, with - as _.
199
+ for source in "$plugin/bindings"/*.swift; do
200
+ printf ' TokamakBindings_%s(host: TokamakHost(app: app, plugin: "%s")),\n' "${plugin_id//-/_}" "$plugin_id" >> "$registry"
201
+ add_plugin_source "$plugin_id" "$source"
202
+ done
183
203
  for source in "$plugin/sources"/*; do
184
204
  case "$source" in
185
- *.swift) swift_sources+=("$source") ;;
205
+ *.swift) add_plugin_source "$plugin_id" "$source" ;;
186
206
  *)
187
207
  echo "Apple plugin source must be Swift: $source" >&2
188
208
  exit 1
@@ -23,13 +23,16 @@ typedef struct {
23
23
 
24
24
  typedef void (*TokamakPluginRequest)(void *context, uint64_t id,
25
25
  const char *plugin, const char *method,
26
- const char *arguments);
26
+ const uint8_t *payload, size_t payload_len);
27
27
 
28
28
  typedef struct {
29
29
  void *context;
30
30
  TokamakPluginRequest call;
31
+ TokamakPluginRequest call_with_body;
32
+ void (*chunk)(void *context, uint64_t id, const uint8_t *payload,
33
+ size_t payload_len, bool failed, bool done);
31
34
  TokamakPluginRequest subscribe;
32
- void (*unsubscribe)(void *context, uint64_t id);
35
+ void (*cancel)(void *context, uint64_t id);
33
36
  } TokamakPluginHandler;
34
37
 
35
38
  void *tokamak_runtime_start(const char *packaged_dir, const char *state_dir,
@@ -47,14 +50,17 @@ uint16_t tokamak_runtime_restore_gateway(const void *runtime, char *error,
47
50
  size_t error_len);
48
51
  void tokamak_runtime_stop(void *runtime);
49
52
  bool tokamak_runtime_emit(const void *runtime, const char *name,
50
- const char *event, uint64_t timeout_ms,
53
+ const uint8_t *event, size_t event_len, uint64_t timeout_ms,
51
54
  TokamakBytes *reply, char *error, size_t error_len);
52
55
  bool tokamak_runtime_fetch(const void *runtime, const char *path,
53
56
  uint64_t timeout_ms, TokamakBytes *body, char *error,
54
57
  size_t error_len);
55
58
  bool tokamak_runtime_set_foreground(const void *runtime, bool foreground);
56
59
  bool tokamak_runtime_is_foreground(const void *runtime);
57
- void tokamak_runtime_reply(const void *runtime, uint64_t id, const char *result);
60
+ void tokamak_runtime_reply(const void *runtime, uint64_t id,
61
+ const uint8_t *payload, size_t payload_len,
62
+ bool failed, bool done);
63
+ void tokamak_runtime_pull(const void *runtime, uint64_t id);
58
64
 
59
65
  int32_t tokamak_runtime_server_authority(const void *runtime, const char *host,
60
66
  TokamakBytes *authority);
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import TokamakRuntime
3
+ import WebKit
3
4
 
4
5
  #if os(iOS)
5
6
  import UIKit
@@ -17,19 +18,15 @@ private let lifecycleDeadline: TimeInterval = 10
17
18
  /// WebView, so plugins receive app events before a page loads and a Worker
18
19
  /// event can run while no page is loaded.
19
20
  final class TokamakApp {
21
+ weak var webView: WKWebView?
22
+ let resources = TokamakResources()
20
23
  /// Every plugin, by ID.
21
24
  private(set) var plugins: [String: any TokamakPlugin] = [:]
25
+ private let runtimeLock = NSLock()
22
26
  private var runtime: Result<RuntimeHandle, Error>?
23
27
  private var waiting: [(Result<RuntimeHandle, Error>) -> Void] = []
24
28
  /// The Worker's calls and subscriptions on the plugins.
25
- private lazy var workerRequests = TokamakPluginRequests<UInt64>(plugins: plugins) {
26
- [unowned self] id, result in
27
- whenStarted { started in
28
- if case .success(let runtime) = started {
29
- runtime.reply(id, result: result)
30
- }
31
- }
32
- }
29
+ private var workerRequests: TokamakPluginRequests<UInt64>!
33
30
  /// Delivers `resume` and `suspend` one at a time, in order.
34
31
  private let lifecycle = DispatchQueue(label: "tokamak.lifecycle")
35
32
  #if os(iOS)
@@ -39,9 +36,22 @@ final class TokamakApp {
39
36
 
40
37
  /// Creates the plugins. Call it before launch finishes.
41
38
  init() {
39
+ plugins["tokamak"] = TokamakCorePlugin(host: TokamakHost(app: self, plugin: "tokamak"))
42
40
  for plugin in tokamakPlugins(app: self) {
43
41
  precondition(plugins.updateValue(plugin, forKey: plugin.id) == nil, "Duplicate plugin ID")
44
42
  }
43
+ workerRequests = TokamakPluginRequests(
44
+ plugins: plugins,
45
+ send: { [unowned self] id, result in
46
+ ready { started in
47
+ if case .success(let runtime) = started { runtime.reply(id, result: result) }
48
+ }
49
+ },
50
+ pull: { [unowned self] id in
51
+ ready { started in
52
+ if case .success(let runtime) = started { runtime.pull(id) }
53
+ }
54
+ })
45
55
  }
46
56
 
47
57
  /// Starts the runtime in the background, which delivers `start` with
@@ -60,15 +70,24 @@ final class TokamakApp {
60
70
  private var workerPlugins: TokamakPluginHandler {
61
71
  TokamakPluginHandler(
62
72
  context: Unmanaged.passUnretained(self).toOpaque(),
63
- call: { context, id, plugin, method, arguments in
64
- TokamakApp.receive(context, id, subscribe: false, plugin, method, arguments)
73
+ call: { context, id, plugin, method, payload, count in
74
+ TokamakApp.receive(context, id, .call, plugin, method, payload, count)
65
75
  },
66
- subscribe: { context, id, plugin, method, arguments in
67
- TokamakApp.receive(context, id, subscribe: true, plugin, method, arguments)
76
+ call_with_body: { context, id, plugin, method, payload, count in
77
+ TokamakApp.receive(context, id, .callWithBody, plugin, method, payload, count)
68
78
  },
69
- unsubscribe: { context, id in
79
+ chunk: { context, id, payload, count, failed, done in
70
80
  let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
71
- DispatchQueue.main.async { app.workerRequests.cancel(id) }
81
+ let payload = count == 0 ? Data() : Data(bytes: payload!, count: count)
82
+ app.workerRequests.chunk(
83
+ id, TokamakPluginResult(payload: payload, failed: failed, done: done))
84
+ },
85
+ subscribe: { context, id, plugin, method, payload, count in
86
+ TokamakApp.receive(context, id, .subscribe, plugin, method, payload, count)
87
+ },
88
+ cancel: { context, id in
89
+ let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
90
+ app.workerRequests.cancel(id)
72
91
  }
73
92
  )
74
93
  }
@@ -77,48 +96,47 @@ final class TokamakApp {
77
96
  private static func receive(
78
97
  _ context: UnsafeMutableRawPointer?,
79
98
  _ id: UInt64,
80
- subscribe: Bool,
99
+ _ kind: TokamakPluginRequest,
81
100
  _ plugin: UnsafePointer<CChar>?,
82
101
  _ method: UnsafePointer<CChar>?,
83
- _ arguments: UnsafePointer<CChar>?
102
+ _ payload: UnsafePointer<UInt8>?,
103
+ _ count: Int
84
104
  ) {
85
105
  let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
86
106
  let (plugin, method) = (String(cString: plugin!), String(cString: method!))
87
- let arguments =
88
- (try? JSONSerialization.jsonObject(
89
- with: Data(String(cString: arguments!).utf8), options: .fragmentsAllowed)) ?? NSNull()
90
- DispatchQueue.main.async {
91
- app.workerRequests.run(
92
- id, subscribe: subscribe, plugin: plugin, method: method, arguments: arguments)
93
- }
107
+ let payload = count == 0 ? Data() : Data(bytes: payload!, count: count)
108
+ app.workerRequests.run(id, kind, plugin: plugin, method: method, payload: payload)
94
109
  }
95
110
 
96
111
  /// Calls `completion` on the main thread once the runtime has started or
97
112
  /// failed to start.
98
113
  func whenStarted(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
99
- dispatchPrecondition(condition: .onQueue(.main))
100
- if let runtime {
101
- completion(runtime)
102
- } else {
103
- waiting.append(completion)
114
+ ready { result in DispatchQueue.main.async { completion(result) } }
115
+ }
116
+
117
+ private func ready(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
118
+ let current = runtimeLock.withLock {
119
+ if runtime == nil { waiting.append(completion) }
120
+ return runtime
104
121
  }
122
+ if let current { completion(current) }
105
123
  }
106
124
 
107
- /// Delivers the event `name` with `event`, JSON-serialisable, to the
125
+ /// Delivers the event `name`, whose encoded data is `payload`, to the
108
126
  /// Worker's listeners on `queue`, and calls `completion` on the main thread
109
- /// with their reply, parsed from JSON, or nil. Fails unless they return
110
- /// within `timeout`, which includes runtime startup. Call it on the main
111
- /// thread.
127
+ /// with their encoded reply, which is empty when none replied. Fails unless
128
+ /// they return within `timeout`, which includes runtime startup. Call it on
129
+ /// the main thread.
112
130
  func emit(
113
131
  _ name: String,
114
- event: Any,
132
+ payload: Data,
115
133
  timeout: TimeInterval,
116
134
  on queue: DispatchQueue = .global(qos: .userInitiated),
117
- completion: @escaping (Result<Any?, Error>) -> Void
135
+ completion: @escaping (Result<Data, Error>) -> Void
118
136
  ) {
119
137
  let deadline = Date() + timeout
120
138
  withRuntime(on: queue, completion: completion) { runtime in
121
- try runtime.emit(name, event: event, timeout: deadline.timeIntervalSinceNow)
139
+ try runtime.emit(name, payload: payload, timeout: deadline.timeIntervalSinceNow)
122
140
  }
123
141
  }
124
142
 
@@ -184,7 +202,7 @@ final class TokamakApp {
184
202
  finish()
185
203
  return
186
204
  }
187
- self.emit(name, event: [String: Any](), timeout: timeout, on: self.lifecycle) { result in
205
+ self.emit(name, payload: Data("{}".utf8), timeout: timeout, on: self.lifecycle) { result in
188
206
  if case .failure(let error) = result {
189
207
  print("tokamak \(name) failed: \(error)")
190
208
  }
@@ -262,9 +280,12 @@ final class TokamakApp {
262
280
  if case .failure(let error) = result {
263
281
  RuntimeHandle.recordStartupFailure(error)
264
282
  }
265
- runtime = result
266
- let callbacks = waiting
267
- waiting.removeAll()
283
+ let callbacks = runtimeLock.withLock {
284
+ runtime = result
285
+ let callbacks = waiting
286
+ waiting.removeAll()
287
+ return callbacks
288
+ }
268
289
  for callback in callbacks {
269
290
  callback(result)
270
291
  }
@@ -316,10 +337,7 @@ final class RuntimeHandle {
316
337
  /// Starts the runtime, which delivers `start` with `foreground`, and whose
317
338
  /// Worker calls `plugins`.
318
339
  init(foreground: Bool, plugins: TokamakPluginHandler) throws {
319
- guard
320
- let appHost = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
321
- !appHost.isEmpty
322
- else {
340
+ guard let appHost = Bundle.main.tokamakHost, !appHost.isEmpty else {
323
341
  throw RuntimeError.configuration("TokamakHost is required")
324
342
  }
325
343
  self.appHost = appHost
@@ -404,26 +422,20 @@ final class RuntimeHandle {
404
422
  return port
405
423
  }
406
424
 
407
- /// Delivers the event `name` with `event`, JSON-serialisable, to the
425
+ /// Delivers the event `name`, whose encoded data is `payload`, to the
408
426
  /// Worker's listeners, blocking until they return or `timeout` passes, and
409
- /// returns their reply, parsed from JSON, or nil.
410
- func emit(_ name: String, event: Any, timeout: TimeInterval) throws -> Any? {
411
- guard JSONSerialization.isValidJSONObject([event]) else {
412
- throw RuntimeError.runtime("the \(name) event is not JSON")
413
- }
414
- let event = String(
415
- decoding: try JSONSerialization.data(withJSONObject: event, options: .fragmentsAllowed),
416
- as: UTF8.self
417
- )
427
+ /// returns their encoded reply, which is empty when none replied.
428
+ func emit(_ name: String, payload: Data, timeout: TimeInterval) throws -> Data {
418
429
  var reply = TokamakBytes()
419
430
  var error = [CChar](repeating: 0, count: 1024)
420
431
  let succeeded = name.withCString { name in
421
- event.withCString { event in
432
+ payload.withUnsafeBytes { payload in
422
433
  error.withUnsafeMutableBufferPointer { error in
423
434
  tokamak_runtime_emit(
424
435
  handle,
425
436
  name,
426
- event,
437
+ payload.bindMemory(to: UInt8.self).baseAddress,
438
+ payload.count,
427
439
  UInt64(max(timeout, 0) * 1000),
428
440
  &reply,
429
441
  error.baseAddress,
@@ -433,11 +445,10 @@ final class RuntimeHandle {
433
445
  }
434
446
  }
435
447
  defer { tokamak_bytes_free(reply) }
436
- guard succeeded, let json = reply.contents else {
448
+ guard succeeded else {
437
449
  throw RuntimeError.runtime(String(cString: error))
438
450
  }
439
- let value = try JSONSerialization.jsonObject(with: json, options: .fragmentsAllowed)
440
- return value is NSNull ? nil : value
451
+ return reply.contents ?? Data()
441
452
  }
442
453
 
443
454
  /// What the app serves at `path`, blocking until it arrives or `timeout`
@@ -476,11 +487,18 @@ final class RuntimeHandle {
476
487
  tokamak_runtime_is_foreground(handle)
477
488
  }
478
489
 
479
- /// Passes a plugin's `result`, JSON-serialisable, to the Worker's call or
480
- /// subscription `id`.
481
- func reply(_ id: UInt64, result: [String: Any]) {
482
- guard let json = try? JSONSerialization.data(withJSONObject: result) else { return }
483
- String(decoding: json, as: UTF8.self).withCString { tokamak_runtime_reply(handle, id, $0) }
490
+ /// Asks the Worker for the next chunk of the body of the call `id`.
491
+ func pull(_ id: UInt64) {
492
+ tokamak_runtime_pull(handle, id)
493
+ }
494
+
495
+ /// Passes an encoded plugin result to its native invocation.
496
+ func reply(_ id: UInt64, result: TokamakPluginResult) {
497
+ result.payload.withUnsafeBytes {
498
+ tokamak_runtime_reply(
499
+ handle, id, $0.bindMemory(to: UInt8.self).baseAddress,
500
+ $0.count, result.failed, result.done)
501
+ }
484
502
  }
485
503
 
486
504
  func serverAuthority(host: String) -> AuthenticationMaterial<Data> {