Skip to content

Commit 24ec34e

Browse files
committed
feat(react-native): bundle callback directive plugin
1 parent 46bf430 commit 24ec34e

12 files changed

Lines changed: 558 additions & 9 deletions

File tree

NativeScript/ffi/shared/jsi/NativeApiJsiCallbacks.h

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ struct NativeApiJsiSignature {
3636
unsigned int implicitArgumentCount = 0;
3737
};
3838

39+
enum class NativeApiJsiCallbackThreadPolicy {
40+
Default,
41+
UI,
42+
JS,
43+
};
44+
45+
NativeApiJsiCallbackThreadPolicy readJsiCallbackThreadPolicy(
46+
Runtime& runtime, Object& functionObject) {
47+
constexpr const char* propertyName = "__nativeScriptCallbackThread";
48+
try {
49+
if (!functionObject.hasProperty(runtime, propertyName)) {
50+
return NativeApiJsiCallbackThreadPolicy::Default;
51+
}
52+
Value policyValue = functionObject.getProperty(runtime, propertyName);
53+
if (!policyValue.isString()) {
54+
return NativeApiJsiCallbackThreadPolicy::Default;
55+
}
56+
std::string policy = policyValue.asString(runtime).utf8(runtime);
57+
if (policy == "ui") {
58+
return NativeApiJsiCallbackThreadPolicy::UI;
59+
}
60+
if (policy == "js") {
61+
return NativeApiJsiCallbackThreadPolicy::JS;
62+
}
63+
} catch (const std::exception&) {
64+
}
65+
return NativeApiJsiCallbackThreadPolicy::Default;
66+
}
67+
3968
bool selectorEndsWithNSErrorParam(const std::string& selectorName) {
4069
constexpr const char* suffix = "error:";
4170
size_t suffixLength = std::strlen(suffix);
@@ -304,13 +333,17 @@ class NativeApiJsiCallback final
304333
NativeApiJsiCallback(Runtime& runtime,
305334
std::shared_ptr<NativeApiJsiBridge> bridge,
306335
std::shared_ptr<NativeApiJsiSignature> signature,
307-
Function function, bool block, bool bindThis = false)
336+
Function function, bool block,
337+
NativeApiJsiCallbackThreadPolicy threadPolicy =
338+
NativeApiJsiCallbackThreadPolicy::Default,
339+
bool bindThis = false)
308340
: runtimeOwner_(retainNativeApiJsiRuntime(runtime)),
309341
runtime_(runtimeOwner_.get()),
310342
bridge_(std::move(bridge)),
311343
signature_(std::move(signature)),
312344
function_(std::make_shared<Function>(std::move(function))),
313345
block_(block),
346+
threadPolicy_(threadPolicy),
314347
bindThis_(bindThis) {
315348
closure_ = static_cast<ffi_closure*>(
316349
ffi_closure_alloc(sizeof(ffi_closure), &executable_));
@@ -421,6 +454,66 @@ class NativeApiJsiCallback final
421454
const auto& jsThreadCallbackInvoker = bridge_->jsThreadCallbackInvoker();
422455
bool currentThreadIsJs =
423456
std::this_thread::get_id() == bridge_->jsThreadId();
457+
458+
auto callOnNativeCallerThread = [&]() {
459+
ScopedNativeCallerThreadJsiCallback callbackScope;
460+
call();
461+
};
462+
auto callOnUIThread = [&]() {
463+
auto runOnUIThread = [&]() {
464+
bool previous = gExecutingDispatchedUINativeCall;
465+
gExecutingDispatchedUINativeCall = true;
466+
callOnNativeCallerThread();
467+
gExecutingDispatchedUINativeCall = previous;
468+
};
469+
if ([NSThread isMainThread]) {
470+
runOnUIThread();
471+
} else {
472+
dispatch_sync(dispatch_get_main_queue(), ^{
473+
runOnUIThread();
474+
});
475+
}
476+
};
477+
auto callOnJSThread = [&]() {
478+
if (currentThreadIsJs) {
479+
call();
480+
return;
481+
}
482+
if (jsThreadCallbackInvoker) {
483+
jsThreadCallbackInvoker(call);
484+
return;
485+
}
486+
if (auto scheduler = bridge_->scheduler()) {
487+
dispatch_semaphore_t done = dispatch_semaphore_create(0);
488+
scheduler->invokeOnJS([call, done]() mutable {
489+
call();
490+
dispatch_semaphore_signal(done);
491+
});
492+
dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER);
493+
return;
494+
}
495+
error = "Native callback was invoked off the JS thread without a JS scheduler.";
496+
};
497+
498+
if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::UI) {
499+
callOnUIThread();
500+
if (!error.empty()) {
501+
if (!recordNativeCallbackException(error)) {
502+
throwNativeApiJsiCallbackException(error);
503+
}
504+
}
505+
return;
506+
}
507+
if (threadPolicy_ == NativeApiJsiCallbackThreadPolicy::JS) {
508+
callOnJSThread();
509+
if (!error.empty()) {
510+
if (!recordNativeCallbackException(error)) {
511+
throwNativeApiJsiCallbackException(error);
512+
}
513+
}
514+
return;
515+
}
516+
424517
bool returnsVoid = signature_->returnType.kind == metagen::mdTypeVoid;
425518
bool activeSynchronousNativeInvocation =
426519
gActiveSynchronousNativeInvocationDepth.load(
@@ -439,8 +532,7 @@ class NativeApiJsiCallback final
439532
gActiveNativeThreadJsiCallbacks.load(std::memory_order_acquire) > 0;
440533
if (direct && !waitForNativeThreadCallback) {
441534
if (nativeCallerThreadCallback) {
442-
ScopedNativeCallerThreadJsiCallback callbackScope;
443-
call();
535+
callOnNativeCallerThread();
444536
} else {
445537
call();
446538
}
@@ -626,6 +718,8 @@ class NativeApiJsiCallback final
626718
std::shared_ptr<NativeApiJsiSignature> signature_;
627719
std::shared_ptr<Function> function_;
628720
bool block_ = false;
721+
NativeApiJsiCallbackThreadPolicy threadPolicy_ =
722+
NativeApiJsiCallbackThreadPolicy::Default;
629723
bool bindThis_ = false;
630724
ffi_closure* closure_ = nullptr;
631725
void* executable_ = nullptr;
@@ -1444,7 +1538,9 @@ bool signatureSupportedForJsiCallback(const NativeApiJsiSignature& signature) {
14441538

14451539
std::shared_ptr<NativeApiJsiCallback> createJsiCallback(
14461540
Runtime& runtime, const std::shared_ptr<NativeApiJsiBridge>& bridge,
1447-
const NativeApiJsiType& type, Function function, bool block) {
1541+
const NativeApiJsiType& type, Function function, bool block,
1542+
NativeApiJsiCallbackThreadPolicy threadPolicy =
1543+
NativeApiJsiCallbackThreadPolicy::Default) {
14481544
if (bridge == nullptr || bridge->metadata() == nullptr ||
14491545
type.signatureOffset == MD_SECTION_OFFSET_NULL) {
14501546
throw facebook::jsi::JSError(
@@ -1461,7 +1557,8 @@ std::shared_ptr<NativeApiJsiCallback> createJsiCallback(
14611557
auto signature =
14621558
std::make_shared<NativeApiJsiSignature>(std::move(*parsed));
14631559
auto callback = std::make_shared<NativeApiJsiCallback>(
1464-
runtime, bridge, std::move(signature), std::move(function), block);
1560+
runtime, bridge, std::move(signature), std::move(function), block,
1561+
threadPolicy);
14651562
if (!block) {
14661563
bridge->retainJsiLifetime(callback);
14671564
}
@@ -1489,7 +1586,8 @@ std::shared_ptr<NativeApiJsiCallback> createJsiMethodCallback(
14891586
auto signature =
14901587
std::make_shared<NativeApiJsiSignature>(std::move(*parsed));
14911588
auto callback = std::make_shared<NativeApiJsiCallback>(
1492-
runtime, bridge, std::move(signature), std::move(function), false, true);
1589+
runtime, bridge, std::move(signature), std::move(function), false,
1590+
NativeApiJsiCallbackThreadPolicy::Default, true);
14931591
bridge->retainJsiLifetime(callback);
14941592
return callback;
14951593
}
@@ -1509,7 +1607,7 @@ std::shared_ptr<NativeApiJsiCallback> createJsiMethodCallback(
15091607
std::make_shared<NativeApiJsiSignature>(std::move(signature));
15101608
auto callback = std::make_shared<NativeApiJsiCallback>(
15111609
runtime, bridge, std::move(sharedSignature), std::move(function), false,
1512-
true);
1610+
NativeApiJsiCallbackThreadPolicy::Default, true);
15131611
bridge->retainJsiLifetime(callback);
15141612
return callback;
15151613
}

NativeScript/ffi/shared/jsi/NativeApiJsiConversion.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,9 +756,10 @@ void convertJsiArgument(Runtime& runtime,
756756
break;
757757
}
758758
if (object.isFunction(runtime)) {
759+
auto threadPolicy = readJsiCallbackThreadPolicy(runtime, object);
759760
auto callback = createJsiCallback(
760761
runtime, bridge, type, object.asFunction(runtime),
761-
type.kind == metagen::mdTypeBlock);
762+
type.kind == metagen::mdTypeBlock, threadPolicy);
762763
void* pointer = callback->functionPointer();
763764
if (type.kind == metagen::mdTypeBlock) {
764765
frame.addLifetime(callback);

packages/react-native/README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,38 @@ await NativeScript.runOnUI(() => {
2727
});
2828
```
2929

30+
Obj-C blocks default to the thread that invoked the block. Wrap callbacks when
31+
you want an explicit thread policy:
32+
33+
```ts
34+
UIView.animateWithDurationAnimationsCompletion(
35+
0.25,
36+
NativeScript.uiInvoker(() => {
37+
view.alpha = 0.5;
38+
}),
39+
NativeScript.jsInvoker((finished) => {
40+
console.log("animation finished", finished);
41+
}),
42+
);
43+
```
44+
45+
The package also includes a Babel plugin for directive-style callbacks:
46+
47+
```ts
48+
someNativeApi(() => {
49+
"use ui";
50+
view.alpha = 1;
51+
});
52+
53+
someNativeApi(() => {
54+
"use js";
55+
console.log("back on JS");
56+
});
57+
```
58+
59+
The transform rewrites those callbacks to `NativeScript.uiInvoker(fn)` and
60+
`NativeScript.jsInvoker(fn)`.
61+
3062
## Defining native UIKit views in JS
3163

3264
Use `defineUIKitView()` to turn a NativeScript-created `UIView` tree into a
@@ -131,6 +163,16 @@ npm run test-rn-turbomodule
131163
});
132164
```
133165

166+
4. To use directive-style callbacks in a bare React Native app, add the bundled
167+
Babel plugin:
168+
169+
```js
170+
module.exports = {
171+
presets: ["module:@react-native/babel-preset"],
172+
plugins: ["@nativescript/react-native/babel-plugin"],
173+
};
174+
```
175+
134176
## Using the package in an Expo app
135177

136178
Expo Go cannot load this package because it contains custom native code. Use an
@@ -159,7 +201,9 @@ Expo development build, EAS Build, or `npx expo run:ios`.
159201
```
160202

161203
The plugin configures iOS for Hermes and the React Native New Architecture,
162-
which are required by this JSI TurboModule.
204+
which are required by this JSI TurboModule. It also adds the
205+
`@nativescript/react-native/babel-plugin` transform to `babel.config.js` so
206+
`"use ui"` and `"use js"` callback directives work in Expo bundles.
163207

164208
3. Prebuild and run the iOS development build:
165209

@@ -192,3 +236,6 @@ Expo development build, EAS Build, or `npx expo run:ios`.
192236
},
193237
});
194238
```
239+
240+
Set `{ "babelPlugin": false }` in the config plugin options if you prefer to add
241+
the Babel plugin manually.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./plugin/babel-plugin');

packages/react-native/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"types": "src/index.d.ts",
2525
"files": [
2626
"app.plugin.js",
27+
"babel-plugin.js",
2728
"plugin",
2829
"src",
2930
"types",

0 commit comments

Comments
 (0)