janela 0.10.0 → 0.10.1
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.
- package/bin/janela.mjs +6 -4
- package/package.json +1 -1
- package/shim/ios/app.cc +330 -0
- package/vendor-webview/core/include/webview/backends.hh +1 -0
- package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +4 -1
- package/vendor-webview/core/include/webview/detail/backends/uikit_webkit.hh +460 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIApplication.hh +80 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIScreen.hh +57 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIView.hh +79 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIViewController.hh +70 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIWindow.hh +67 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/uikit.hh +43 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +4 -2
- package/vendor-webview/core/include/webview/macros.h +12 -2
- package/shim/ios/app.mm +0 -386
package/bin/janela.mjs
CHANGED
|
@@ -418,14 +418,16 @@ function buildIos(root, conf, buildDir, outDir) {
|
|
|
418
418
|
const bundle = join(outDir, `${conf.name}.app`);
|
|
419
419
|
mkdirSync(bundle, { recursive: true });
|
|
420
420
|
const sdk = capture(["xcrun", "--sdk", "iphonesimulator", "--show-sdk-path"]);
|
|
421
|
-
//
|
|
422
|
-
//
|
|
421
|
+
// Plain C++: the shell drives the webview through webview.h's UIKit
|
|
422
|
+
// backend, which uses the Objective-C runtime rather than Objective-C, so
|
|
423
|
+
// no .mm and no -fobjc-arc. Blocks still need -fblocks for libdispatch.
|
|
423
424
|
run([
|
|
424
425
|
"xcrun", "clang++",
|
|
425
|
-
join(KIT, "shim", "ios", "app.
|
|
426
|
+
join(KIT, "shim", "ios", "app.cc"),
|
|
426
427
|
"-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
|
|
427
428
|
"-isysroot", sdk,
|
|
428
|
-
"-
|
|
429
|
+
"-std=c++17", "-O2", "-fblocks",
|
|
430
|
+
`-I${join(KIT, "vendor-webview", "core", "include")}`,
|
|
429
431
|
"-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
|
|
430
432
|
lib,
|
|
431
433
|
"-o", join(bundle, conf.name),
|
package/package.json
CHANGED
package/shim/ios/app.cc
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// janela's iOS shell.
|
|
2
|
+
//
|
|
3
|
+
// UIKit owns the run loop and the app's TypeScript is a linked scriptc
|
|
4
|
+
// library we call into — the mirror image of the desktop shim, where
|
|
5
|
+
// TypeScript owns main() and drives a C library over FFI.
|
|
6
|
+
//
|
|
7
|
+
// Everything webview-shaped lives behind webview.h's UIKit backend: creating
|
|
8
|
+
// the web view, injecting the page bootstrap, binding the invoke channel,
|
|
9
|
+
// evaluating JavaScript, and settling a page promise that was answered later.
|
|
10
|
+
// What is left here is janela's own: the due-ordered timer queue, the table of
|
|
11
|
+
// page replies we are holding, file I/O, and the scriptc library's C ABI.
|
|
12
|
+
//
|
|
13
|
+
// scriptc builds iOS as a library only, and library mode links no event loop
|
|
14
|
+
// (SC4005) and creates no threads, so the shell owns the clock and every
|
|
15
|
+
// blocking call.
|
|
16
|
+
|
|
17
|
+
#include "webview.h"
|
|
18
|
+
|
|
19
|
+
#include <dispatch/dispatch.h>
|
|
20
|
+
|
|
21
|
+
#include <cstdlib>
|
|
22
|
+
#include <cstring>
|
|
23
|
+
#include <fstream>
|
|
24
|
+
#include <map>
|
|
25
|
+
#include <sstream>
|
|
26
|
+
#include <string>
|
|
27
|
+
|
|
28
|
+
// ---- the scriptc library's C ABI (see the generated profile) ---------------
|
|
29
|
+
extern "C" {
|
|
30
|
+
void jl_init(void);
|
|
31
|
+
void jl_reset(void);
|
|
32
|
+
void jl_set_panic_sink(void (*fn)(void *, const char *, size_t, const char *,
|
|
33
|
+
size_t),
|
|
34
|
+
void *ctx);
|
|
35
|
+
int32_t jl_set_callback(const char *name, void (*fn)(void), void *ctx);
|
|
36
|
+
void jl_handle_invoke(const char *cmd, size_t cmd_len, const char *args,
|
|
37
|
+
size_t args_len, char **out, size_t *out_len);
|
|
38
|
+
void jl_index_html(char **out, size_t *out_len);
|
|
39
|
+
void jl_on_timer(double id);
|
|
40
|
+
void jl_on_fs_done(double id, bool ok, const char *payload, size_t payload_len);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
namespace {
|
|
44
|
+
|
|
45
|
+
webview::webview *g_webview = nullptr;
|
|
46
|
+
|
|
47
|
+
/// Copy a library-owned result out of its arena and release it. Results live
|
|
48
|
+
/// until the next jl_reset(), so nothing may hold the pointer past this call.
|
|
49
|
+
std::string take_result(char *out, size_t out_len) {
|
|
50
|
+
std::string s = out ? std::string(out, out_len) : std::string{"null"};
|
|
51
|
+
jl_reset();
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
void panic_sink(void *, const char *sym, size_t sym_len, const char *msg,
|
|
56
|
+
size_t msg_len) {
|
|
57
|
+
std::fprintf(stderr, "[janela] host panic in %.*s: %.*s\n", (int)sym_len, sym,
|
|
58
|
+
(int)msg_len, msg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- what the shell owns for the library ----------------------------------
|
|
62
|
+
//
|
|
63
|
+
// THE RULE (upstream scriptc #263): a channel handler must never re-enter the
|
|
64
|
+
// library. Every handler below only records what it was told and returns; the
|
|
65
|
+
// re-entry happens from a dispatched block, i.e. at the top of a later turn
|
|
66
|
+
// with no library frame beneath it. A breach silently appears to work, so this
|
|
67
|
+
// has to hold by construction rather than by testing.
|
|
68
|
+
|
|
69
|
+
/// Page replies whose answer was not ready when handle_invoke returned:
|
|
70
|
+
/// janela's pending id -> the webview binding id we have not resolved yet.
|
|
71
|
+
std::map<long long, std::string> g_held;
|
|
72
|
+
/// Envelopes that arrived before we knew the binding id — a command that
|
|
73
|
+
/// resolves synchronously settles during its own dispatch, before the
|
|
74
|
+
/// {"pending":id} envelope has made it back to us.
|
|
75
|
+
std::map<long long, std::string> g_early;
|
|
76
|
+
|
|
77
|
+
void settle(const std::string &binding_id, const std::string &envelope) {
|
|
78
|
+
if (g_webview) {
|
|
79
|
+
g_webview->resolve(binding_id, 0, envelope);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Answer the page for a pending id, whichever half arrived first.
|
|
84
|
+
void resolve_pending(long long pending_id, const std::string &envelope) {
|
|
85
|
+
auto it = g_held.find(pending_id);
|
|
86
|
+
if (it != g_held.end()) {
|
|
87
|
+
std::string binding_id = it->second;
|
|
88
|
+
g_held.erase(it);
|
|
89
|
+
settle(binding_id, envelope);
|
|
90
|
+
} else {
|
|
91
|
+
g_early[pending_id] = envelope; // handle_invoke has not returned yet
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Register the binding id a pending answer belongs to.
|
|
96
|
+
void hold_pending(long long pending_id, const std::string &binding_id) {
|
|
97
|
+
auto it = g_early.find(pending_id);
|
|
98
|
+
if (it != g_early.end()) {
|
|
99
|
+
std::string envelope = it->second;
|
|
100
|
+
g_early.erase(it);
|
|
101
|
+
settle(binding_id, envelope); // it resolved synchronously; answer now
|
|
102
|
+
} else {
|
|
103
|
+
g_held[pending_id] = binding_id;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- channels the library calls (TS -> shell) ------------------------------
|
|
108
|
+
|
|
109
|
+
/// Park a continuation with us and call back when it comes due. A zero delay
|
|
110
|
+
/// still goes through dispatch_after(0), which posts to the next turn — that
|
|
111
|
+
/// is app.defer(), and it costs no timer.
|
|
112
|
+
void host_schedule(void *, double id, double ms) {
|
|
113
|
+
int64_t delay = (int64_t)(ms > 0 ? ms : 0);
|
|
114
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC),
|
|
115
|
+
dispatch_get_main_queue(), ^{
|
|
116
|
+
jl_on_timer(id); // fresh turn: no library frame beneath us
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// The library has an answer for a held page reply.
|
|
121
|
+
void host_settle(void *, double pending_id, const char *env, size_t env_len) {
|
|
122
|
+
auto key = (long long)pending_id;
|
|
123
|
+
std::string envelope(env ? env : "null", env ? env_len : 4);
|
|
124
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
125
|
+
resolve_pending(key, envelope);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- the file queue --------------------------------------------------------
|
|
130
|
+
//
|
|
131
|
+
// Blocking I/O happens here, never on the main queue and never inside the
|
|
132
|
+
// library, which links no threads of its own.
|
|
133
|
+
//
|
|
134
|
+
// An iOS app has no useful working directory and cannot see host paths, so a
|
|
135
|
+
// relative path is taken as relative to the app's Documents directory — the
|
|
136
|
+
// one place a sandboxed app can freely read and write. HOME is the app's
|
|
137
|
+
// container, so this needs no Foundation call.
|
|
138
|
+
std::string resolve_path(const std::string &path) {
|
|
139
|
+
if (!path.empty() && path[0] == '/') {
|
|
140
|
+
return path;
|
|
141
|
+
}
|
|
142
|
+
const char *home = std::getenv("HOME");
|
|
143
|
+
std::string base = home ? std::string(home) + "/Documents" : std::string(".");
|
|
144
|
+
return base + "/" + path;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
dispatch_queue_t fs_queue() {
|
|
148
|
+
static dispatch_queue_t q =
|
|
149
|
+
dispatch_queue_create("dev.janela.fs", DISPATCH_QUEUE_SERIAL);
|
|
150
|
+
return q;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Takes the payload BY VALUE: a block captures a reference parameter as a
|
|
154
|
+
// reference, so a temporary passed by the caller would already be destroyed by
|
|
155
|
+
// the time the block runs on the main queue, and the library would decode
|
|
156
|
+
// freed memory.
|
|
157
|
+
void finish_fs(double id, bool ok, std::string payload) {
|
|
158
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
159
|
+
jl_on_fs_done(id, ok, payload.c_str(), payload.size());
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
void host_read_file(void *, double id, const char *path, size_t path_len) {
|
|
164
|
+
std::string p = resolve_path(std::string(path, path_len));
|
|
165
|
+
dispatch_async(fs_queue(), ^{
|
|
166
|
+
std::ifstream in(p, std::ios::binary);
|
|
167
|
+
if (!in) {
|
|
168
|
+
finish_fs(id, false, "ENOENT: no such file or directory, open '" + p + "'");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
std::ostringstream ss;
|
|
172
|
+
ss << in.rdbuf();
|
|
173
|
+
finish_fs(id, true, ss.str());
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void host_write_file(void *, double id, const char *path, size_t path_len,
|
|
178
|
+
const char *data, size_t data_len) {
|
|
179
|
+
std::string p = resolve_path(std::string(path, path_len));
|
|
180
|
+
std::string d(data, data_len);
|
|
181
|
+
dispatch_async(fs_queue(), ^{
|
|
182
|
+
std::ofstream out(p, std::ios::binary | std::ios::trunc);
|
|
183
|
+
if (!out) {
|
|
184
|
+
finish_fs(id, false, "EACCES: could not open '" + p + "' for writing");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
out.write(d.data(), (std::streamsize)d.size());
|
|
188
|
+
out.close();
|
|
189
|
+
if (!out) {
|
|
190
|
+
finish_fs(id, false, "EIO: write failed for '" + p + "'");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
finish_fs(id, true, "");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- host -> page: the event channel ---------------------------------------
|
|
198
|
+
|
|
199
|
+
/// Minimal JSON string quoting, for splicing an event name into a JS call.
|
|
200
|
+
std::string js_quote(const std::string &raw) {
|
|
201
|
+
std::string out = "\"";
|
|
202
|
+
for (char c : raw) {
|
|
203
|
+
switch (c) {
|
|
204
|
+
case '"':
|
|
205
|
+
out += "\\\"";
|
|
206
|
+
break;
|
|
207
|
+
case '\\':
|
|
208
|
+
out += "\\\\";
|
|
209
|
+
break;
|
|
210
|
+
case '\n':
|
|
211
|
+
out += "\\n";
|
|
212
|
+
break;
|
|
213
|
+
case '\r':
|
|
214
|
+
out += "\\r";
|
|
215
|
+
break;
|
|
216
|
+
case '\t':
|
|
217
|
+
out += "\\t";
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
if ((unsigned char)c < 0x20) {
|
|
221
|
+
char buf[7];
|
|
222
|
+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
223
|
+
out += buf;
|
|
224
|
+
} else {
|
|
225
|
+
out += c;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
out += "\"";
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
void emit_event(void *, const char *name, size_t name_len, const char *payload,
|
|
234
|
+
size_t payload_len) {
|
|
235
|
+
std::string js = "window.__wvEmit(" +
|
|
236
|
+
js_quote(std::string(name, name_len)) + "," +
|
|
237
|
+
std::string(payload, payload_len) + ");";
|
|
238
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
239
|
+
if (g_webview) {
|
|
240
|
+
g_webview->eval(js);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- the page-side bridge --------------------------------------------------
|
|
246
|
+
//
|
|
247
|
+
// webview.h binds __janelaInvoke and injects the plumbing that turns it into a
|
|
248
|
+
// promise; this adds the janela surface on top, so `janela/api` and a
|
|
249
|
+
// project's frontend work exactly as they do on desktop.
|
|
250
|
+
|
|
251
|
+
const char *const kBootstrap = R"JS(
|
|
252
|
+
window.__wvListeners = {};
|
|
253
|
+
window.janela = {
|
|
254
|
+
invoke: function (cmd, args) {
|
|
255
|
+
return window.__janelaInvoke(cmd, JSON.stringify(args === undefined ? null : args))
|
|
256
|
+
.then(function (env) {
|
|
257
|
+
if (env && env.ok) return env.value;
|
|
258
|
+
throw new Error((env && env.error) || 'janela: invoke failed');
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
listen: function (event, cb) {
|
|
262
|
+
if (!window.__wvListeners[event]) window.__wvListeners[event] = [];
|
|
263
|
+
window.__wvListeners[event].push(cb);
|
|
264
|
+
return function () {
|
|
265
|
+
var a = window.__wvListeners[event] || [];
|
|
266
|
+
var i = a.indexOf(cb);
|
|
267
|
+
if (i >= 0) a.splice(i, 1);
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
window.__wvEmit = function (event, payload) {
|
|
272
|
+
var cbs = window.__wvListeners[event] || [];
|
|
273
|
+
for (var i = 0; i < cbs.length; i++) cbs[i](payload);
|
|
274
|
+
};
|
|
275
|
+
)JS";
|
|
276
|
+
|
|
277
|
+
/// The page's invoke channel. `req` is a JSON array of the two arguments the
|
|
278
|
+
/// bootstrap passed: [cmd, argsJson].
|
|
279
|
+
void on_invoke(const std::string &binding_id, const std::string &req, void *) {
|
|
280
|
+
std::string cmd = webview::detail::json_parse(req, "", 0);
|
|
281
|
+
std::string args = webview::detail::json_parse(req, "", 1);
|
|
282
|
+
|
|
283
|
+
char *out = nullptr;
|
|
284
|
+
size_t out_len = 0;
|
|
285
|
+
jl_handle_invoke(cmd.c_str(), cmd.size(), args.c_str(), args.size(), &out,
|
|
286
|
+
&out_len);
|
|
287
|
+
std::string reply = take_result(out, out_len);
|
|
288
|
+
|
|
289
|
+
// An async command answers later: the library returns {"pending":<id>} and
|
|
290
|
+
// we hold the page's promise until host_settle() tells us what it is.
|
|
291
|
+
std::string pending = webview::detail::json_parse(reply, "pending", 0);
|
|
292
|
+
if (!pending.empty()) {
|
|
293
|
+
hold_pending(std::strtoll(pending.c_str(), nullptr, 10), binding_id);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Otherwise the reply is already the envelope the bootstrap expects.
|
|
298
|
+
settle(binding_id, reply);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
} // namespace
|
|
302
|
+
|
|
303
|
+
int main() {
|
|
304
|
+
// Registration is a pure store and is legal before init; the panic sink and
|
|
305
|
+
// every channel must be in place before any TypeScript runs, since setup()
|
|
306
|
+
// executes during jl_init().
|
|
307
|
+
jl_set_panic_sink(panic_sink, nullptr);
|
|
308
|
+
if (jl_set_callback("hostSchedule", (void (*)(void))host_schedule, nullptr) ||
|
|
309
|
+
jl_set_callback("hostSettle", (void (*)(void))host_settle, nullptr) ||
|
|
310
|
+
jl_set_callback("hostReadFile", (void (*)(void))host_read_file, nullptr) ||
|
|
311
|
+
jl_set_callback("hostWriteFile", (void (*)(void))host_write_file,
|
|
312
|
+
nullptr) ||
|
|
313
|
+
jl_set_callback("janelaEmit", (void (*)(void))emit_event, nullptr)) {
|
|
314
|
+
std::fprintf(stderr, "[janela] could not register a host channel\n");
|
|
315
|
+
}
|
|
316
|
+
jl_init();
|
|
317
|
+
|
|
318
|
+
webview::webview w(false, nullptr);
|
|
319
|
+
g_webview = &w;
|
|
320
|
+
w.init(kBootstrap);
|
|
321
|
+
w.bind("__janelaInvoke", on_invoke, nullptr);
|
|
322
|
+
|
|
323
|
+
char *out = nullptr;
|
|
324
|
+
size_t out_len = 0;
|
|
325
|
+
jl_index_html(&out, &out_len);
|
|
326
|
+
w.set_html(take_result(out, out_len));
|
|
327
|
+
|
|
328
|
+
w.run(); // enters UIApplicationMain; does not return
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
@@ -55,7 +55,10 @@
|
|
|
55
55
|
#include <memory>
|
|
56
56
|
#include <string>
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
// <objc/objc-runtime.h> is an umbrella that only exists in the macOS SDK;
|
|
59
|
+
// these two are present on every Apple platform.
|
|
60
|
+
#include <objc/message.h>
|
|
61
|
+
#include <objc/runtime.h>
|
|
59
62
|
|
|
60
63
|
namespace webview {
|
|
61
64
|
namespace detail {
|