Skip to content

Commit e5410ed

Browse files
authored
Enable logbox for unhandled exceptions that go from JS back to native (#98)
In a nutshell, this change makes unhandled JS errors that bubble back up to native code (through our JSI related code paths) display correctly in the React Native logbox/redbox so you can see what error happened and where. See the big comment in DispatchFunction.h for more details. - Add a new BabylonReactNativeShared lib, since we will likely share more code going forward. - Add DispatchFunction.h containing the CreateJsRuntimeDispatcher function. This is where the work is done to re-expose the unhandled error in a way that it is displayed in the React Native logbox. - Add a library dependency on BabylonReactNativeShared for both the iOS and Android Babylon React Native CMakeLists.txt files. - Use CreateJsRuntimeDispatcher in both the iOS and Android Babylon React Native interop layers. With these changes, when an unhandled exception occurs in JavaScript that is called from our native code, it now shows correctly in logbox.
1 parent eca1d7c commit e5410ed

File tree

8 files changed

+74
-30
lines changed

8 files changed

+74
-30
lines changed

Modules/@babylonjs/react-native/android/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/src/")
3434
set(BABYLON_NATIVE_DIR "${CMAKE_CURRENT_LIST_DIR}/../submodules/BabylonNative")
3535
add_subdirectory(${BABYLON_NATIVE_DIR} ${BABYLON_NATIVE_DIR}/build/Android_${CMAKE_ANDROID_ARCH_ABI}/)
3636

37+
set(BABYLON_REACT_NATIVE_SHARED_DIR "${CMAKE_CURRENT_LIST_DIR}/../shared")
38+
add_subdirectory(${BABYLON_REACT_NATIVE_SHARED_DIR} ${CMAKE_CURRENT_BINARY_DIR}/shared)
39+
3740
add_library(fbjni SHARED IMPORTED)
3841
set_target_properties(fbjni PROPERTIES
3942
IMPORTED_LOCATION ${FBJNI_LIBPATH}/${ANDROID_ABI}/libfbjni.so
@@ -63,6 +66,7 @@ target_link_libraries(BabylonNative
6366
fbjni
6467
jsi
6568
turbomodulejsijni
69+
BabylonReactNativeShared
6670
AndroidExtensions
6771
Graphics
6872
JsRuntime

Modules/@babylonjs/react-native/android/src/main/cpp/BabylonNativeInterop.cpp

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include <jsi/jsi.h>
2323
#include <ReactCommon/CallInvokerHolder.h>
2424

25+
#include <DispatchFunction.h>
26+
27+
using namespace facebook;
28+
2529
namespace Babylon
2630
{
2731
namespace
@@ -36,19 +40,10 @@ namespace Babylon
3640
{
3741
public:
3842
// This class must be constructed from the JavaScript thread
39-
Native(facebook::jsi::Runtime* jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, ANativeWindow* windowPtr)
40-
: m_env{ Napi::Attach<facebook::jsi::Runtime&>(*jsiRuntime) }
43+
Native(jsi::Runtime& jsiRuntime, std::shared_ptr<react::CallInvoker> callInvoker, ANativeWindow* windowPtr)
44+
: m_env{ Napi::Attach<jsi::Runtime&>(jsiRuntime) }
4145
{
42-
JsRuntime::DispatchFunctionT dispatchFunction =
43-
[env = m_env, callInvoker](std::function<void(Napi::Env)> func)
44-
{
45-
callInvoker->invokeAsync([env, func = std::move(func)]
46-
{
47-
func(env);
48-
});
49-
};
50-
51-
m_runtime = &JsRuntime::CreateForJavaScript(m_env, dispatchFunction);
46+
m_runtime = &JsRuntime::CreateForJavaScript(m_env, CreateJsRuntimeDispatcher(m_env, jsiRuntime, callInvoker));
5247

5348
auto width = static_cast<size_t>(ANativeWindow_getWidth(windowPtr));
5449
auto height = static_cast<size_t>(ANativeWindow_getHeight(windowPtr));
@@ -135,10 +130,10 @@ extern "C" JNIEXPORT void JNICALL Java_com_reactlibrary_BabylonNativeInterop_res
135130

136131
extern "C" JNIEXPORT jlong JNICALL Java_com_reactlibrary_BabylonNativeInterop_create(JNIEnv* env, jclass obj, jlong jsiRuntimeRef, jobject jsCallInvokerHolder, jobject surface)
137132
{
138-
auto jsiRuntime = reinterpret_cast<facebook::jsi::Runtime*>(jsiRuntimeRef);
139-
auto callInvoker = facebook::jni::alias_ref<facebook::react::CallInvokerHolder::javaobject> {reinterpret_cast<facebook::react::CallInvokerHolder::javaobject>(jsCallInvokerHolder)}->cthis()->getCallInvoker();
133+
auto jsiRuntime = reinterpret_cast<jsi::Runtime*>(jsiRuntimeRef);
134+
auto callInvoker = jni::alias_ref<react::CallInvokerHolder::javaobject> {reinterpret_cast<react::CallInvokerHolder::javaobject>(jsCallInvokerHolder)}->cthis()->getCallInvoker();
140135
ANativeWindow* windowPtr = ANativeWindow_fromSurface(env, surface);
141-
auto native = new Babylon::Native(jsiRuntime, callInvoker, windowPtr);
136+
auto native = new Babylon::Native(*jsiRuntime, callInvoker, windowPtr);
142137
return reinterpret_cast<intptr_t>(native);
143138
}
144139

Modules/@babylonjs/react-native/ios/BabylonNative.cpp

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@
1919
#include <sstream>
2020
#include <unistd.h>
2121

22+
#include <DispatchFunction.h>
23+
2224
namespace Babylon
2325
{
2426
using namespace facebook;
2527

2628
class Native::Impl
2729
{
2830
public:
29-
Impl(facebook::jsi::Runtime* jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker)
30-
: env{ Napi::Attach<facebook::jsi::Runtime&>(*jsiRuntime) }
31+
Impl(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker)
32+
: env{ Napi::Attach<facebook::jsi::Runtime&>(jsiRuntime) }
3133
, jsCallInvoker{ callInvoker }
3234
{
3335
}
@@ -39,23 +41,14 @@ namespace Babylon
3941
Plugins::NativeInput* nativeInput{};
4042
};
4143

42-
Native::Native(facebook::jsi::Runtime* jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, void* windowPtr, size_t width, size_t height)
44+
Native::Native(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, void* windowPtr, size_t width, size_t height)
4345
: m_impl{ std::make_unique<Native::Impl>(jsiRuntime, callInvoker) }
4446
{
4547
dispatch_sync(dispatch_get_main_queue(), ^{
4648
m_impl->m_graphics = Graphics::InitializeFromWindow<void*>(windowPtr, width, height);
4749
});
4850

49-
JsRuntime::DispatchFunctionT dispatchFunction =
50-
[env = m_impl->env, callInvoker = m_impl->jsCallInvoker](std::function<void(Napi::Env)> func)
51-
{
52-
callInvoker->invokeAsync([env, func = std::move(func)]
53-
{
54-
func(env);
55-
});
56-
};
57-
58-
m_impl->runtime = &JsRuntime::CreateForJavaScript(m_impl->env, std::move(dispatchFunction));
51+
m_impl->runtime = &JsRuntime::CreateForJavaScript(m_impl->env, CreateJsRuntimeDispatcher(m_impl->env, jsiRuntime, callInvoker));
5952

6053
m_impl->m_graphics->AddToJavaScript(m_impl->env);
6154

Modules/@babylonjs/react-native/ios/BabylonNative.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Babylon
99
{
1010
public:
1111
// This class must be constructed from the JavaScript thread
12-
Native(facebook::jsi::Runtime* jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, void* windowPtr, size_t width, size_t height);
12+
Native(facebook::jsi::Runtime& jsiRuntime, std::shared_ptr<facebook::react::CallInvoker> callInvoker, void* windowPtr, size_t width, size_t height);
1313
~Native();
1414
void Refresh(void* windowPtr, size_t width, size_t height);
1515
void Resize(size_t width, size_t height);

Modules/@babylonjs/react-native/ios/BabylonNativeInterop.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ + (void)setCurrentNativeInstance:(RCTBridge*)bridge mtkView:(MTKView*)mtkView wi
132132

133133
jsi::Runtime* jsiRuntime = GetJSIRuntime(currentBridge);
134134
if (jsiRuntime) {
135-
currentNativeInstance = std::make_unique<Babylon::Native>(GetJSIRuntime(currentBridge), currentBridge.jsCallInvoker, (__bridge void*)mtkView, width, height);
135+
currentNativeInstance = std::make_unique<Babylon::Native>(*jsiRuntime, currentBridge.jsCallInvoker, (__bridge void*)mtkView, width, height);
136136
}
137137
}
138138

Modules/@babylonjs/react-native/ios/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/")
2323
set(BABYLON_NATIVE_DIR "${CMAKE_CURRENT_LIST_DIR}/../submodules/BabylonNative")
2424
add_subdirectory(${BABYLON_NATIVE_DIR} ${BABYLON_NATIVE_DIR}/build/ios/)
2525

26+
set(BABYLON_REACT_NATIVE_SHARED_DIR "${CMAKE_CURRENT_LIST_DIR}/../shared")
27+
add_subdirectory(${BABYLON_REACT_NATIVE_SHARED_DIR} ${CMAKE_CURRENT_BINARY_DIR}/shared)
28+
2629
add_library(BabylonNative
2730
${CMAKE_CURRENT_LIST_DIR}/BabylonNative.h
2831
${CMAKE_CURRENT_LIST_DIR}/BabylonNative.cpp)
@@ -38,6 +41,7 @@ target_link_libraries(BabylonNative
3841
Graphics
3942
jsi
4043
reactnative
44+
BabylonReactNativeShared
4145
JsRuntime
4246
NativeWindow
4347
NativeEngine
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
add_library(BabylonReactNativeShared INTERFACE)
2+
target_include_directories(BabylonReactNativeShared INTERFACE ".")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
#include <Babylon/JsRuntime.h>
4+
5+
#include <jsi/jsi.h>
6+
#include <ReactCommon/CallInvoker.h>
7+
8+
namespace Babylon
9+
{
10+
using namespace facebook;
11+
12+
// Creates a JsRuntime::DispatchFunctionT that integrates with the React Native execution environment.
13+
inline JsRuntime::DispatchFunctionT CreateJsRuntimeDispatcher(Napi::Env env, jsi::Runtime& jsiRuntime, std::shared_ptr<react::CallInvoker> callInvoker)
14+
{
15+
return [env, &jsiRuntime, callInvoker](std::function<void(Napi::Env)> func)
16+
{
17+
// Ideally we would just use CallInvoker::invokeAsync directly, but currently it does not seem to integrate well with the React Native logbox.
18+
// To work around this, we wrap all functions in a try/catch, and when there is an exception, we do the following:
19+
// 1. Call the JavaScript setImmediate function.
20+
// 2. Have the setImmediate callback call back into native code (throwFunc).
21+
// 3. Re-throw the exception from throwFunc.
22+
// This works because:
23+
// 1. setImmediate queues the callback, and that queue is drained immediately following the invocation of the function passed to CallInvoker::invokeAsync.
24+
// 2. The immediates queue is drained as part of the class bridge, which knows how to display the logbox for unhandled exceptions.
25+
// In the future, CallInvoker::invokeAsync likely will properly integrate with logbox, at which point we can remove the try/catch and just call func directly.
26+
callInvoker->invokeAsync([env, &jsiRuntime, func{std::move(func)}]
27+
{
28+
try
29+
{
30+
func(env);
31+
}
32+
catch (...)
33+
{
34+
auto ex{std::current_exception()};
35+
auto setImmediate{jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "setImmediate")};
36+
auto throwFunc{jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "throwFunc"), 0,
37+
[ex](jsi::Runtime &, const jsi::Value &, const jsi::Value *, size_t) -> jsi::Value
38+
{
39+
std::rethrow_exception(ex);
40+
})};
41+
setImmediate.call(jsiRuntime, {std::move(throwFunc)});
42+
}
43+
});
44+
};
45+
}
46+
}

0 commit comments

Comments
 (0)