From 34c5b11927f8f2036f2113968119431737dfc6ba Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 11 Jan 2024 17:23:38 +0100 Subject: [PATCH] feat: Re-throw error on JS side instead of just logging on native side (#2366) * feat: Re-throw error on JS side instead of just logging on native side * fix: Fix proxy * fix: Fix app crash by only logging error * fix: Use `global.ErrorUtils` (from reanimated) --- .../java-bindings/JFrameProcessor.cpp | 20 +++-------- package/ios/Frame Processor/FrameProcessor.mm | 20 +++-------- package/src/FrameProcessorPlugins.ts | 36 +++++++++++++++++++ package/src/hooks/useFrameProcessor.ts | 4 +++ 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/package/android/src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp b/package/android/src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp index 4f1a71c..b755c6d 100644 --- a/package/android/src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp +++ b/package/android/src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp @@ -36,22 +36,12 @@ void JFrameProcessor::callWithFrameHostObject(const std::shared_ptrgetWorkletRuntime(); - try { - // Wrap HostObject as JSI Value - auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); - jsi::Value jsValue(std::move(argument)); + // Wrap HostObject as JSI Value + auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); + jsi::Value jsValue(std::move(argument)); - // Call the Worklet with the Frame JS Host Object as an argument - _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); - } catch (jsi::JSError& jsError) { - // JS Error occured, print it to console. - const std::string& message = jsError.getMessage(); - - _workletContext->invokeOnJsThread([message](jsi::Runtime& jsRuntime) { - auto logFn = jsRuntime.global().getPropertyAsObject(jsRuntime, "console").getPropertyAsFunction(jsRuntime, "error"); - logFn.call(jsRuntime, jsi::String::createFromUtf8(jsRuntime, "Frame Processor threw an error: " + message)); - }); - } + // Call the Worklet with the Frame JS Host Object as an argument + _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); } void JFrameProcessor::call(jni::alias_ref frame) { diff --git a/package/ios/Frame Processor/FrameProcessor.mm b/package/ios/Frame Processor/FrameProcessor.mm index 601b021..c75e8eb 100644 --- a/package/ios/Frame Processor/FrameProcessor.mm +++ b/package/ios/Frame Processor/FrameProcessor.mm @@ -34,22 +34,12 @@ using namespace facebook; // Call the Frame Processor on the Worklet Runtime jsi::Runtime& runtime = _workletContext->getWorkletRuntime(); - try { - // Wrap HostObject as JSI Value - auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); - jsi::Value jsValue(std::move(argument)); + // Wrap HostObject as JSI Value + auto argument = jsi::Object::createFromHostObject(runtime, frameHostObject); + jsi::Value jsValue(std::move(argument)); - // Call the Worklet with the Frame JS Host Object as an argument - _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); - } catch (jsi::JSError& jsError) { - // JS Error occured, print it to console. - auto message = jsError.getMessage(); - - _workletContext->invokeOnJsThread([message](jsi::Runtime& jsRuntime) { - auto logFn = jsRuntime.global().getPropertyAsObject(jsRuntime, "console").getPropertyAsFunction(jsRuntime, "error"); - logFn.call(jsRuntime, jsi::String::createFromUtf8(jsRuntime, "Frame Processor threw an error: " + message)); - }); - } + // Call the Worklet with the Frame JS Host Object as an argument + _workletInvoker->call(runtime, jsi::Value::undefined(), &jsValue, 1); } - (void)call:(Frame* _Nonnull)frame { diff --git a/package/src/FrameProcessorPlugins.ts b/package/src/FrameProcessorPlugins.ts index ee99048..bf94d3d 100644 --- a/package/src/FrameProcessorPlugins.ts +++ b/package/src/FrameProcessorPlugins.ts @@ -35,6 +35,10 @@ interface TVisionCameraProxy { * ``` */ initFrameProcessorPlugin: (name: string, options?: Record) => FrameProcessorPlugin | undefined + /** + * Throws the given error. + */ + throwJSError: (error: unknown) => void } const errorMessage = 'Frame Processors are not available, react-native-worklets-core is not installed!' @@ -44,6 +48,9 @@ let isAsyncContextBusy = { value: false } let runOnAsyncContext = (_frame: Frame, _func: () => void): void => { throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage) } +let throwJSError = (error: unknown): void => { + throw error +} try { assertJSIAvailable() @@ -51,6 +58,24 @@ try { // eslint-disable-next-line @typescript-eslint/no-var-requires const { Worklets } = require('react-native-worklets-core') as typeof TWorklets + const throwErrorOnJS = Worklets.createRunInJsFn((message: string, stack: string | undefined) => { + const error = new Error() + error.message = message + error.stack = stack + error.name = 'Frame Processor Error' + // @ts-expect-error this is react-native specific + error.jsEngine = 'VisionCamera' + // From react-native: + // @ts-ignore the reportFatalError method is an internal method of ErrorUtils not exposed in the type definitions + global.ErrorUtils.reportFatalError(error) + }) + throwJSError = (error) => { + 'worklet' + const safeError = error as Error | undefined + const message = safeError != null && 'message' in safeError ? safeError.message : 'Frame Processor threw an error.' + throwErrorOnJS(message, safeError?.stack) + } + isAsyncContextBusy = Worklets.createSharedValue(false) const asyncContext = Worklets.createContext('VisionCamera.async') runOnAsyncContext = Worklets.createRunInContextFn((frame: Frame, func: () => void) => { @@ -58,6 +83,9 @@ try { try { // Call long-running function func() + } catch (e) { + // Re-throw error on JS Thread + throwJSError(e) } finally { // Potentially delete Frame if we were the last ref const internal = frame as FrameInternal @@ -81,6 +109,7 @@ let proxy: TVisionCameraProxy = { setFrameProcessor: () => { throw new CameraRuntimeError('system/frame-processors-unavailable', errorMessage) }, + throwJSError: throwJSError, } if (hasWorklets) { // Install native Frame Processor Runtime Manager @@ -103,6 +132,7 @@ export const VisionCameraProxy: TVisionCameraProxy = { initFrameProcessorPlugin: proxy.initFrameProcessorPlugin, removeFrameProcessor: proxy.removeFrameProcessor, setFrameProcessor: proxy.setFrameProcessor, + throwJSError: throwJSError, // TODO: Remove this in the next version // @ts-expect-error getFrameProcessorPlugin: (name, options) => { @@ -116,6 +146,12 @@ export const VisionCameraProxy: TVisionCameraProxy = { declare global { // eslint-disable-next-line no-var var __frameProcessorRunAtTargetFpsMap: Record | undefined + // eslint-disable-next-line no-var + var __ErrorUtils: + | { + reportFatalError: (error: unknown) => void + } + | undefined } function getLastFrameProcessorCall(frameProcessorFuncId: string): number { diff --git a/package/src/hooks/useFrameProcessor.ts b/package/src/hooks/useFrameProcessor.ts index eeab5cf..e94a61d 100644 --- a/package/src/hooks/useFrameProcessor.ts +++ b/package/src/hooks/useFrameProcessor.ts @@ -1,6 +1,7 @@ import { DependencyList, useMemo } from 'react' import type { Frame, FrameInternal } from '../Frame' import { FrameProcessor } from '../CameraProps' +import { VisionCameraProxy } from '../FrameProcessorPlugins' /** * Create a new Frame Processor function which you can pass to the ``. @@ -20,6 +21,9 @@ export function createFrameProcessor(frameProcessor: FrameProcessor['frameProces try { // Call sync frame processor frameProcessor(frame) + } catch (e) { + // Re-throw error on JS Thread + VisionCameraProxy.throwJSError(e) } finally { // Potentially delete Frame if we were the last ref (no runAsync) internal.decrementRefCount()