react-native
react-native copied to clipboard
feat: Allow accessing `jsi::Value` on `RawProps`
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).
| 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
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!
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
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
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.
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...
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)
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.
To be completely precise, it's not RawValue, it's Props that eagerly converts incoming RawProps to folly::dynamic. Happens only on Android.
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?
To be completely precise, it's not
RawValue, it'sPropsthat eagerly converts incomingRawPropstofolly::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.
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...
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.
Hmm okay I see, thanks.
So just to be on the same page, is this the same idea that you're proposing?
- Someone creates a view component using my React Native Framework (say
<Camera>with 2 props:device,frameProcessor, both are custom types/jsi::HostObjects) - 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 justnumbers - In my JS View component (
Camera.tsx), I then intercept all props in therender()function, and for each prop I do something likemakeValueGlobalAndGetReferenceId(device)(same forframeProcessor), and pass that to the React Native View component - 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
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...
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).
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
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
Notes
- I moved the
RawPropsParserfromComponentDescriptortoConcreteComponentDescriptoras it was used only there - For this to work with
RawPropswhich usesRawPropsParserwe have to implement a interface / abstract class which we use inRawProps
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
Note: In this PoC example the
JsiPropParserisn't optimized for performance yet (ie. passing dangling pointers, re-creatingjsi::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
I feel like the pro to this approach is that:
- We offer a migration path for
RawValueto remove thefolly::dynamictypes (we can start experimenting with this integration, at some point bring over theJsiPropParserand 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)
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.
Roger that, will open up a new PR with such a change in the coming days! Thanks for the feedback :)
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
RawPropsParserto enable the jsi variant - Pass a custom instance of
RawPropsParsertoConcreteComponentProviderfor components as an opt-in
- Pass a flag to
obsolete now