mrousavy / nitro

🔥 Insanely fast native C++, Swift or Kotlin modules with a statically compiled binding layer to JSI
https://nitro.margelo.com
MIT License
598 stars 15 forks source link

feat: Direct JNI converters to JSI #121

Open mrousavy opened 1 month ago

mrousavy commented 1 month ago

Before, a Java/Kotlin HybridObject always went through C++/std types, even if we discarded them right away since we pass them to Java.

For example, for Arrays this looked like this:

flowchart LR
    jsi[jsi::Array]-->cpp[std::vector]-->jni[jni::JArray]

Now with this PR, we generate bindings directly to JSI:

flowchart LR
    jsi[jsi::Array]-->jni[jni::JArray]

This is much more efficient, especially for Arrays, as we completely avoid C++/std types.

Additionally, since every HybridObject is a C++ object at it's base (since we also want to be able to use Kotlin/Swift types from C++ as if it was any conventional C++ class), we also need to generate bindings to C++:

flowchart LR
    jsi[jsi::Array]-->jni[jni::JArray]
    cpp[std::vector]-->jni[jni::JArray]

This looks like this in a HybridObject:

class JHybridMathSpec: public HybridMathSpec {
public:
  // Called only if you use HybridMathSpec in C++ and call it's virtual method. Internally, this calls getSomethingJNI()
  std::vector<double> getSomething();
  // Bound to JS to override the prototype method so we go directly into JNI
  jni::JArrayClass<double> getSomethingJNI();
}
mrousavy commented 1 month ago

It works!!! 🥳

image

mrousavy commented 1 month ago

So now it works (finally! 🥳), but the performance improvements are in the 5%-10% area, which is less than I hoped for.

We can still optimize a few things like trying to construct JString without std::string... I'll look into that.

mrousavy commented 1 month ago

Strings in Kotlin got a bit faster though:

Screenshot 2024-09-14 at 19 20 32

mrousavy commented 1 month ago

The performance improvements are much less than what I originally anticipated. It seems like Nitro was already really fast on Android.

Going directly to JNI didn't really give any benefits other than a 20% speed improvement for Strings, but the rest roughly stayed the same as the C++ layer is so efficient already.

Also, with this PR the complexity of Kotlin HybridObjects greatly increased, because every property and method got generated twice - once for Kotlin (JNI <> JS), and once for C++ (JNI <> C++, to provide access to C++ if needed). This is very complex and potentially error prone.. Not sure if it's a good decision to do that.

I'll profile and investigate more on this, and think about if the added complexity in the code-generator CLI is worth the small performance improvements here.