react-native icon indicating copy to clipboard operation
react-native copied to clipboard

feat: Allow accessing `jsi::Value` on `RawProps`

Open mrousavy opened this issue 1 year ago • 16 comments

Summary:

I'm working on a new React Native Framework. It uses pure C++/JSI to create, mount and update a View under Fabric.

Unfortunately the RawProps type abstraction is not enough in my case, I need the actual jsi::Value (which might be a jsi::HostObject, NativeState, or other custom types I created).

Now my idea is to avoid going the RawPropsParser route and instead directly access the RawProps' jsi::Value (including it's jsi::Runtime) to go my route.

This will be safe since I immediately convert the jsi::Value to other C++ types, and I don't keep that instance (nor the jsi::Runtime) in memory.

My previous approach was extending RawPropsParser, but that's a bit more complex since friendship is not inheritable in C++ and I also feel like we don't need the whole prop parsing loop. In my case, all of the parsing is statically generated with templates - there will be no iterations.

If there's a simpler or better alternative to this, I'm all ears - but from what I'm seeing in the codebase there is no better direct point to intercept all jsi::Values for view components. Please correct me if I'm wrong (cc @philIip ?)

Changelog:

[INTERNAL] [CHANGED] - Allow accessing RawProps' jsi::Value directly for React Native Frameworks

Test Plan:

I receive RawProps (e.g. in ImageProps.cpp), and directly access jsi::Value from there to convert the object to a C++ object (or individual C++ keys).

mrousavy avatar Jun 15 '24 13:06 mrousavy

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 20,365,766 +76
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 23,569,723 +60
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: fe06cbfbf332a999e065bdc13ef5cdeed20c5517 Branch: main

analysis-bot avatar Jun 15 '24 13:06 analysis-bot

cc @cortinico sorry for pinging but please lmk if this is a good or bad idea, and if there's a better way to get the props as jsi::Values. thanks!

mrousavy avatar Jun 18 '24 11:06 mrousavy

cc @cortinico sorry for pinging but please lmk if this is a good or bad idea, and if there's a better way to get the props as jsi::Values. thanks!

@philIip is the best reviewer here

cortinico avatar Jun 19 '24 18:06 cortinico

hmmm my gut is telling me we shouldn't allow this. raw props is supposed to obfuscate the "type" of value bag and this change essentially reverses that. is this to support the frameProcessor lambda prop?

cc @javache @sammy-SC

philIip avatar Jun 25 '24 05:06 philIip

I think your high-level use-case here (allowing complex HostObject props to be passed through) makes sense, but we need to figure out a more scalable way to expose this. RawProps can represent both folly::dynamic as well as jsi::Value, and we manage how this get accessed so we can change the internals from diff-based to full JS representation based.

Also keep in mind that while Android parses props here, it will pass only a folly::dynamic representation of it through to native, and you'd need use State or other mechanisms to make the HostObject available.

javache avatar Jun 25 '24 08:06 javache

Okay I see. I only tested on iOS so far, my bad.

What do you suggest then? I need a way to access a view component's props as jsi::Values...

mrousavy avatar Jun 25 '24 09:06 mrousavy

Something @dmytrorykun came across recently is that RawValue eagerly converts the incoming jsi::Value to a folly::dynamic representation, something we want to move away from. So if we can could get that supported (similar to RawProps supporting both representations), we could allow prop conversion (eg using the existing bridging logic)

javache avatar Jun 25 '24 10:06 javache

Yea I have a similar implementation to the bridging interfaces you built. I just browsed through them, impressive work!

My implementation (not yet OSS) is pretty similar, but has a few important differences. Especially around functions/callbacks (Function/Promise), and custom types (HostObjects/NativeState).

So are you saying that I should use RN's Cxx bridging interface, or is there a way I can use my own? Accessing jsi::Value directly on props.

mrousavy avatar Jun 25 '24 10:06 mrousavy

To be completely precise, it's not RawValue, it's Props that eagerly converts incoming RawProps to folly::dynamic. Happens only on Android.

dmytrorykun avatar Jun 25 '24 11:06 dmytrorykun

And in RawProps we are storing an actual jsi::Value and converting it to a folly::dynamic, as opposed to actually storing a folly::dynamic right?

https://github.com/facebook/react-native/blob/f903f3483794f0e0e0b05ac24ab761344c319d6b/packages/react-native/ReactCommon/react/renderer/core/RawProps.cpp#L174-L184

So then the approach of accessing jsi::Value at this stage (RawProps) would work?

mrousavy avatar Jun 25 '24 11:06 mrousavy

To be completely precise, it's not RawValue, it's Props that eagerly converts incoming RawProps to folly::dynamic. Happens only on Android.

No, I'm talking about this code in RawPropsParser

https://github.com/facebook/react-native/blob/f903f3483794f0e0e0b05ac24ab761344c319d6b/packages/react-native/ReactCommon/react/renderer/core/RawPropsParser.cpp#L140-L141

This is where we need to hold on to the jsi::Value instead.

javache avatar Jun 25 '24 11:06 javache

So just for my understanding; is it correct that on Android, the jsi::Value is already gone after RawProps? (-> folly::dynamic)

but we need to figure out a more scalable way to expose this.

@javache do you have any alternative solutions?

I mean I could try starting fully from scratch to look into mounting views in an entirely different way, I'm not too familiar with how the react renderer / fabric works but to me this would sound like I need to re-invent A TON of stuff, which is not a good idea. I want to reuse Yoga, the react renderer, and even inherit props from UIView if possible, while still getting access to props as jsi::Values...

mrousavy avatar Jun 25 '24 17:06 mrousavy

is it correct that on Android, the jsi::Value is already gone after RawProps

It's gone once we complete parsing, ie when you have the Props object.

@javache do you have any alternative solutions?

I'm afraid not right now. We're actively looking at this code for perf reasons, and mapping it more closely to jsi::Value is definitely something we're interested in, but it's a non-trivial change. For now, I think the simplest option is to use a (C++) TurboModule to store these JSI values, and pass a (numeric) reference to the stored values as a prop instead.

javache avatar Jun 27 '24 21:06 javache

Hmm okay I see, thanks.

So just to be on the same page, is this the same idea that you're proposing?

  1. Someone creates a view component using my React Native Framework (say <Camera> with 2 props: device, frameProcessor, both are custom types/jsi::HostObjects)
  2. Instead of using React Native's view system to somehow pass those props, I just generate a dummy view component that also has the same props (device, frameProcessor), but instead of being any type they are just numbers
  3. In my JS View component (Camera.tsx), I then intercept all props in the render() function, and for each prop I do something like makeValueGlobalAndGetReferenceId(device) (same for frameProcessor), and pass that to the React Native View component
  4. In the React Native view component I then look up the numbers in my global reference table and convert them back from their actual values

JS:

class Camera {

  render() {
    const { device, frameProcessor } = this.props
    const deviceRef = makeValueGlobalAndGetReferenceId(device)
    const frameProcessorRef = makeValueGlobalAndGetReferenceId(frameProcessor)

    return <NativeCamera device={deviceRef} frameProcessor={frameProcessorRef} />
  }
}

C++:

std::unordered_map<double, jsi::Value> globals;
static double latestRefId = 0;
auto func = [](jsi::Runtime&, const jsi::Value&, jsi::Value* args, size_t) -> jsi::Value {
  double referenceId = latestRefId++;
  // store given value in global (TODO: this is UNSAFE, because it escapes this function and we dont know if it will be consumed right away)
  globals[referenceId] = std::move(args[0]);
  return jsi::Value(referenceId);
}
runtime.global().setProperty("makeValueGlobalAndGetReferenceId", jsi::Function::createFromHostFunction(..., func));

My React Native Framework's middleman C++:

void updateProps() {
  // well, this just converts the numbers (deviceRef, frameProcessorRef) to jsi::Values again.
  // but this is also unsafe since we are no longer on the JS thread here afaik.
  // also we are not 100% sure if values will be consumed (e.g. concurrent updates canceling a batch?)
}

Is that what you had in mind as well? This sounds like a really hacky solution, and my TODO comments are a bit concerning

mrousavy avatar Jul 02 '24 17:07 mrousavy

Another problem I see with the global number reference <-> jsi::Value map approach is that view component functions aren't really trivial to implement.

E.g. for a <Camera> I'd have a takePhoto(..) function which returns a Photo jsi::HostObject.

How would I implement this? I could create a global jsi::Function that performs takePhoto(...) and returns a jsi::HostObject, but how do I get a reference to the Camera View in there?

nativeId? findNodeHandle?

I know this isn't trivial at all, but at first I was thinking that maybe somewhat reinventing the wheel here and building my own JSI based view components (including prop updaters and view component functions) would be a better approach.

For that I'd need access to the jsi::Value in RawProps though, unless there is a lower level at which I can get props?

I don't want to re-implement the whole thing starting from React.createElement because then I'd probably break things like Reanimated, as that works through setNativeProps...

mrousavy avatar Jul 02 '24 19:07 mrousavy

Yes, the approach I described has a number of short-comings. All props parsing from React commits does happen on the JS thread though, so accessing these globals should be safe.

We are actively looking at this part of code-base as part of performance improvements, but do not have any immediate plans to make closer integration with jsi::Value part of props parsing.

I don't think we want to pull in this PR as-is, as it's quite broad and will limit us in how we can evolve this going forward (same reason the operator() folly::dynamic is deprecated here).

javache avatar Jul 03 '24 09:07 javache

Hey @javache! After our discussion at react native london we wanted to revisit this topic.

My proposal to move forward with this would be to make the prop parser configurable, so by default the existing RawPropsParser is being used, but can be switched with alternative implementations that enable us to access the jsi::Value directly:

1. Add an optional path for RawValue to hold a runtime + jsi value pair:

https://github.com/hannojg/react-native/blob/00c4d052e4f8a381d25dba73bc60603bb28ba4ca/packages/react-native/ReactCommon/react/renderer/core/RawValue.h#L74-L97

CleanShot 2024-11-21 at 17 59 24

2. Make the RawPropsParser configurable on the level of the ConcreteComponentDescriptor

https://github.com/hannojg/react-native/blob/7232c90e922f760f0b97b4f6f74640d1e895a29c/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h#L32

CleanShot 2024-11-21 at 18 00 51

Notes

  • I moved the RawPropsParser from ComponentDescriptor to ConcreteComponentDescriptor as it was used only there
  • For this to work with RawProps which uses RawPropsParser we have to implement a interface / abstract class which we use in RawProps

3. Use a custom RawPropsParser in our component that wants to opt-in to use jsi prop values directly

Now our custom component can opt into using a custom prop parser, which enables us to store any prop we want to handle with jsi directly as nativeProp

https://github.com/hannojg/react-native/blob/7232c90e922f760f0b97b4f6f74640d1e895a29c/packages/rn-tester/ManualComponent/ManualFabricComponentView.mm#L63-L77

CleanShot 2024-11-21 at 18 03 22

Note: In this PoC example the JsiPropParser isn't optimized for performance yet (ie. passing dangling pointers, re-creating jsi::Values, re-calculating prop names which seems expensive, etc)

4. Get jsi::Value from RawValue in our concrete props implementation

Now in our concrete props implementation we can get the RawValue and read the jsi::Value from it directly:

https://github.com/hannojg/react-native/blob/7232c90e922f760f0b97b4f6f74640d1e895a29c/packages/rn-tester/ManualComponent/ManualFabricComponentView.mm#L27-L42

CleanShot 2024-11-21 at 18 07 55


I feel like the pro to this approach is that:

  • We offer a migration path for RawValue to remove the folly::dynamic types (we can start experimenting with this integration, at some point bring over the JsiPropParser and start to migrate core components)
  • We make the Prop parsing configurable

I still have to look into how we can bridge the value over to android though. I reckon that for our use case not too many changes will be needed, as we will own the bridging part from C++ to the view manager in Java/Kotlin.

Let me know what you all are thinking! 😊

(A PoC PR is here: https://github.com/facebook/react-native/pull/47717)

hannojg avatar Nov 21 '24 17:11 hannojg

Thanks for listing those options @hannojg!

Given that we want to move away from folly::dynamic as the intermediate representation, I'd want to have RawValue support both jsi::Value and folly::dynamic, which should include all the existing type casting API's too, so this becomes a transparent upgrade.

Using templates would be an interesting approach to avoid a ton of std::variant runtime checking to know which path to take, and allow testing this on a component by component basis.

While designing this, consider how we could 1) fully remove the folly::dynamic codepath in the future 2) test an incremental rollout of this, where we could choose at runtime which implementation to use.

javache avatar Nov 27 '24 13:11 javache

Roger that, will open up a new PR with such a change in the coming days! Thanks for the feedback :)

hannojg avatar Nov 27 '24 14:11 hannojg

Update:

I implemented:

  • https://github.com/facebook/react-native/pull/48047

which adds a new constructor to the RawValue class and a feature flag that, when enabled, will use RawValue with the jsi::Values directly.


Consider how we could fully remove the folly::dynamic codepath in the future

We can basically track down all places that create RawValue using folly::dynamic, and change them to pass the jsi Runtime and Value directly.

Test an incremental rollout of this, where we could choose at runtime which implementation to use.

I think this is covered in the PR by using a react native feature flag

Testing this on a component by component basis.

For this, I opened a separate PR on top of the mentioned one, that allows us to:

  • https://github.com/hannojg/react-native/pull/2
    • Pass a flag to RawPropsParser to enable the jsi variant
    • Pass a custom instance of RawPropsParser to ConcreteComponentProvider for components as an opt-in

hannojg avatar Dec 05 '24 12:12 hannojg

obsolete now

mrousavy avatar Feb 18 '25 11:02 mrousavy