Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow accessing jsi::Value on RawProps #44966

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

mrousavy
Copy link
Contributor

@mrousavy mrousavy commented Jun 15, 2024

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).

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Jun 15, 2024
@analysis-bot
Copy link

analysis-bot commented Jun 15, 2024

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: fe06cbf
Branch: main

@mrousavy mrousavy marked this pull request as draft June 16, 2024 21:25
@mrousavy mrousavy changed the title feat: Allow extending RawPropsParser feat: Allow accessing jsi::Value on RawProps Jun 17, 2024
@mrousavy mrousavy marked this pull request as ready for review June 17, 2024 09:27
@mrousavy
Copy link
Contributor Author

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!

@cortinico cortinico requested a review from philIip June 19, 2024 18:23
@cortinico
Copy link
Contributor

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

@philIip
Copy link
Contributor

philIip commented Jun 25, 2024

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

@javache
Copy link
Member

javache commented Jun 25, 2024

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.

@mrousavy
Copy link
Contributor Author

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...

@javache
Copy link
Member

javache commented Jun 25, 2024

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)

@mrousavy
Copy link
Contributor Author

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.

@dmytrorykun
Copy link
Contributor

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

@mrousavy
Copy link
Contributor Author

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?

RawProps::operator folly::dynamic() const noexcept {
switch (mode_) {
case Mode::Empty:
return folly::dynamic::object();
case Mode::JSI:
return jsi::dynamicFromValue(
*runtime_, value_, ignoreYogaStyleProps_ ? isYogaStyleProp : nullptr);
case Mode::Dynamic:
return dynamic_;
}
}

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

@javache
Copy link
Member

javache commented Jun 25, 2024

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

rawProps.values_.push_back(
RawValue(jsi::dynamicFromValue(runtime, value)));

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

@mrousavy
Copy link
Contributor Author

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...

@javache
Copy link
Member

javache commented Jun 27, 2024

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.

@mrousavy
Copy link
Contributor Author

mrousavy commented Jul 2, 2024

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
Copy link
Contributor Author

mrousavy commented Jul 2, 2024

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...

@javache
Copy link
Member

javache commented Jul 3, 2024

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants