Git Product home page Git Product logo

jni-bind's Introduction

JNI Bind

Ubuntu Build

JNI Bind is a new metaprogramming library that provides syntactic sugar for C++ => Java/Kotlin. It is header only and provides sophisticated type conversion with compile time validation of method calls and field accesses.

It requires clang enabled at C++17 or later, is compatible with Android, and is unit / E2E tested on x86/ARM toolchains.

Many features and optimisations are included:

  • Object Management
  • Compile time method name and argument checking
  • Static caching of IDs multi-threading support and compile time signature generation.
  • Classes native construction, argument validation, lifetime support, method and field support.
  • Classloaders native construction, object "buildability".
  • JVM Management single-process multiple JVM lifecycles.
  • Strings easy syntax for inline argument construction.
  • Arrays inline object construction for method arguments, efficient pinning of existing spans.
  • And much more!

Curious to try it? Check out the Godbolt sample!

If you're enjoying JNI Bind, or just want to support it, please consider adding a GitHub ⭐️!

Table of Contents

Quick Intro

JNI is notoriously difficult to use, and numerous libraries exist to try to reduce this complexity. These libraries usually require code generation (or extensive macro use) which leads to brittle implementation, indexes poorly, and still requires domain expertise in JNI (making code difficult to maintain).

JNI Bind is header only (no auto-generation), and it generates robust, easily maintained, and expressive code. It obeys the regular RAII idioms of C++17 and can help separate JNI symbols in compilation. Classes are provided in static constexpr definitions which can be shared across different implementations enabling code re-use.

This is a sample Java class and it's correspondingJNI Bind class definition:

package com.project;
public class Clazz {
 int Foo(float f, String s) {...}
}
#include "jni_bind_release.h"

static constexpr jni::Class kClass {
  "com/project/clazz", jni::Method { "Foo", jni::Return<jint>{}, jni::Params<jfloat, jstring>{}},

jni::LocalObject<kClass> obj { jobject_to_wrap };
obj("Foo", 1.5f, "argString");
// obj("Bar", 1.5, "argString");  // won't compile (good).

There are sample tests which can be a good way to see some example code. Consider starting with with context_test_jni, object_test_helper_jni.h and ContextTest.java.

Installation without Bazel

If you want to jump right in, copy jni_bind_release.h into your own project. The header itself is an automatically generated "flattened" version of the Bazel dependency set, so this documentation is a simpler introduction to JNI Bind than reading the header directly.

You are responsible for ensuring #include <jni.h> compiles when you include JNI Bind.

Installation with Bazel

If you're already using Bazel add the following to your WORKSPACE:

http_archive(
  name = "jni-bind",
  urls = ["https://github.com/google/jni-bind/archive/refs/tags/Release-1.0.1-beta.zip"],
  strip_prefix = "jni-bind-Release-1.0.1-beta",
)

Then include @jni-bind//:jni_bind (not :jni_bind_release) and #include "jni_bind.h".

JNI is sometimes difficult to get working in Bazel, so if you don't have an environment already also add this to your WORKSPACE, and follow the pattern of the BUILD target below.

# Rules Jvm.
RULES_JVM_EXTERNAL_TAG = "4.2"
RULES_JVM_EXTERNAL_SHA = "cd1a77b7b02e8e008439ca76fd34f5b07aecb8c752961f9640dea15e9e5ba1ca"

http_archive(
    name = "rules_jvm_external",
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    sha256 = RULES_JVM_EXTERNAL_SHA,
    url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)
cc_library(
    name = "Foo",
    hdrs = [
        "jni_bind_release.h",
    ],
    srcs = [
        "Foo.cc",
        "@local_jdk//:jni_header",
        "@local_jdk//:jni_md_header-linux",
    ],
    includes = [
        "external/local_jdk/include",
        "external/local_jdk/include/linux",
    ],
)

There are easy to lift samples in javatests/com/jnibind/test/. If you want try building these samples (or to copy the BUILD configuration) you can clone this repo.

cd ~
git clone https://github.com/google/jni-bind.git
cd jni-bind
bazel test  --cxxopt='-std=c++17' --repo_env=CC=clang ...

Usage

JVM Lifecycle

JNI Bind requires some minor bookkeeping in order to ensure acccess to a valid JNIEnv*. To do this, create a jni::JvmRef whose lifetime extends past any JNI Bind call (*a function local static is a reasonable way to do this, although sanitizers will flag this as a memory leak, so all tests explicitly manage the lifetime of the jni::jvmRef).

The simplest way to from the JavaVM* in your JNI_OnLoad call. This object's lifetime must outlive all JNI calls.

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* pjvm, void* reserved) {
  static auto jvm{std::make_unique<jni::JvmRef<jni::kDefaultJvm>>(pjvm)};
  return JNI_VERSION_1_6;
}

⚠️ If you using another JNI Library initialise JNI Bind after. JNI Bind "attaches" the thread explicitly through JNI, and some libraries behaviour will be conditional on this being unset. If JNI Bind discovers a thread has been attached previously it will not attempt to tear the thread down on teardown.

You can also build a jni::jvmRef from any JNIEnv*.

Classes

Class definitions are the basic mechanism by which you interact with Java through JNI:

static constexpr jni::Class kClass{"com/full/class/name/JavaClassName", jni::Field..., jni::Method... };

jni::Class definitions are static (the class names of any Java object is known in advance). Instances of these classes are created at runtime using jni::LocalObject or jni::GlobalObject.

Local and Global Objects

Local and global objects manage lifetimes of underlying jobjects using the normal RAII mechanism of C++. jni::LocalObject always fall off scope at the conclusion of the surrounding JNI call and are valid only to a single thread, however jni::GlobalObject may be held indefinitely and are thread safe.

jni::LocalObject is built by either wrapping a jobject passed to native JNI from Java, or constructing a new object from native (see Constructors).

When jni::LocalObject or jni::GlobalObject falls off scope, it will unpin the underlying jobject, making it available for garbage collection by the JVM. If you want to to prevent this, call Release(). This is useful to return a jobject back from native, or to simply pass to another native component that isn't JNI Bind aware. Calling methods for a released object is undefined.

When possible try to avoid using raw jobject. Managing lifetimes with regular JNI is difficult, e.g. jobject can mean either local or global object (the former will be automatically unpinned at the end of the JNI call, but the latter won't and must be deleted exactly once).

Because jobject does not uniquely identify its underlying storage, it is presumed to always be local. If you want to build a global, you must use either jni::PromoteToGlobal or jni::AdoptGlobal. e.g.

jobject obj1, obj2, obj3;
jni::LocalObject local_obj {obj1}; // Fine, given local semantics.
// jni::GlobalObject global_obj {obj2}; // Illegal, obj2 could be local or global.
jni::GlobalObject global_obj_1 {PromoteToGlobal{}, obj2}; // obj2 will be promoted.
jni::GlobalObject global_obj_2 {AdoptGlobal{}, obj3}; // obj3 will *not* be promoted.

When using a jobject you may add NewRef{} which creates a new local reference, or AdoptLocal{} which takes full ownership.

Because JNI objects passed to native should never be deleted, NewRef is used by default (so that LocalObject may always call delete). A non-deleting FastLocal that does not delete may be added in the future. In general you shouldn't need to worry about this.

Sample C++, Sample Java

Methods

A jni::Method is described as a method name, a jni::Return, and an optional jni::Params containing a variadic pack of zero values of the desired type. If a class definition contains jni::Methods in its definition then corresponding objects constructed will be imbued with operator(). Using this operator with the corresponding method name will invoke the corresponding method.

static constexpr jni::Class kClass {
   "com/project/kClass",
   jni::Method{"intMethod", jni::Return{jint{}},
   jni::Method {"floatMethod", jni::Return{jfloat{}}, jni::Params{ jint{}, jfloat{} }},
   jni::Method {"classMethod", jni::Return{kClassFromOtherHeader} }
};

jni::LocalObject<kClass> runtime_object{jobj};
int int_val = runtime_object("intMethod");
float float_val = runtime_object("floatMethod");
jni::LocalObject<kClassFromOtherHeader> class_val { runtime_object("classMethod") };

Methods will follow the rules laid out in Type Conversion Rules. Invalid method names won't compile, and jmethodIDs are cached on your behalf. Method lookups are compile time, there is no hash lookup cost.

Sample C++, Sample Java

Fields

A jni::Field is described as a field name and a zero value of the underlying type. If a class definition contains jni::Fields in its definition then corresponding objects constructed will be imbued with operator[]. Using this operator with the corresponding field name will provide a proxy object with two methods Get and Set.

static constexpr jni::Class kClass {
  "kClassName",
  jni::Field {"intField", jint{} },
};

jni::LocalObject<kClass> runtime_object{jobj};
runtime_object["intField"].Set(5);
runtime_object["intField"].Get(); // = 5;

Accessing and setting fields will follow the rules laid out in Type Conversion Rules. Accessing invalid field names won't compile, and jfieldIDs are cached on your behalf.

Sample C++, Sample Java

Constructors

If you want to create a new Java object from native code, you can define a jni::Constructor, or use the default constructor. If you omit a constructor the default constructor is called, but if any are specified you must explicitly define a no argument constructor.

static constexpr jni::Class kSomeClass{...};

static constexpr jni::Class kClass {
   "com/google/Class",
   jni::Constructor{jint{}, kSomeClass},
   jni::Constructor{jint{}},
   jni::Constructor{},
};

jni::LocalObject<kClass> obj1{123, jni::LocalObject<kSomeClass>{}};
jni::LocalObject<kClass> obj2{123};
jni::LocalObject<kClass> obj3{};  // Compiles only because of jni::Constructor{}.

Constructors follow the arguments rules laid out in Type Conversion Rules.

Sample C++.

Type Conversion Rules

All JNI types have corresponding JNI Bind types. These types are used differently depending on their context. Sometimes multiple types are valid as an argument, and you can use them interchangeably, however types are strictly enforced, so you can't pass arguments that might otherwise implictly cast.

JNI C API Jni Bind Declaration Types valid when used as arg Return Type
void
jboolean jboolean jboolean, bool jint
jint jint jint, int jint
jfloat jfloat, float jfloat jfloat
jdouble jdouble, double jdouble jdouble
jlong jlong, long jlong jlong
jobject jni::Class kClass jobject, jni::LocalObject, jni::GlobalObject1 jni::LocalObject
jstring jstring jstring, jni::LocalString, jni::GlobalString, jni::LocalString
char*, std::string
jarray jni::Array jarray, jni::LocalArray, , jlongArray jni::LocalArray
jbooleanArray, jbyteArray, jcharArray,
jfloatArray, jintArray
jobjectarray jni::Array<jobject, kClass> jarray, jni::LocalArray<jobject, kClass>, jni::LocalArray

More conversions will be added later and this table will be updated as they are. In particular wchar views into jstrings will offer a zero copy view into java strings. Also, jarray will support std::span views for its underlying types.

Advanced Usage

Strings

Strings are slightly atypical as they are a regular Java class (java/lang/String) but have a separate JNI type, jstring. They therefore have two separate JNI Bind types: jni::LocalString and jni::GlobalString.

Unlike jni::LocalObject, you must explicitly pin the underlying string data to access it (arrays also follow this paradigm). To "view" into the string, you must call Pin() which returns a jni::UtfStringView. You can call ToString() to get a std::string_view into the string.

jni::LocalString new_string{"TestString"};             // builds a new java/lang/String instance.
jni::UtfStringView utf_string_view = new_string.Pin();
std::string_view jni_string_view = utf_string_view.ToString();

jni::UtfStringView will immediately pin memory associated with the jstring, and release on leaving scope. This will always make an expensive copy, as strings are natively represented in Java as Unicode (C++20 will offer a compatible std::string_view but C++17 does not).

Sample C++, Sample Java

Forward Class Declarations

Sometimes you need to reference a class that doesn't yet have a corresponding JNI Bind definition (possibly due to a circular dependency or self reference). This can be obviated by defining the class inline.

using ::jni::Class;
using ::jni::Constructor;
using ::jni::Return;
using ::jni::Method;
using ::jni::Params;

static constexpr Class kClass {
  "com/project/kClass",
  Method{"returnsNothing", Return<void>{}, Params{Class{"kClass2"}},
  Method{"returnsKClass", Return{Class{"com/project/kClass"}},           // self referential
  Method{"returnsKClass2", Return{Class{"com/project/kClass2"}},         // undefined
};

static constexpr Class kClass2 {
  "com/project/kClass2",
  Constructor{ Class{"com/project/kClass2"} },  // self referential
  Method{"Foo", Return<void>{}}
};

LocalObject<kClass> obj1{};
obj1("returnsNothing", LocalObject<kClass2>{});        // correctly forces kClass2 for arg
LocalObject<kClass> obj2{ obj1("returnsKClass") };     // correctly forces kClass for return
LocalObject shallow_obj{} = obj1("returnsKClass");     // returns unusable but name safe kClass
// shallow_obj("Foo");                                 // this won't compile as it's only a forward decl
LocalObject<kClass> rich_obj{std::move(shallow_obj)};  // promotes the object to a usable version
LocalObject<kClass2> { obj1("returnsKClass2") };       // materialised from a temporary shallow rvalue

Note, if you use the output of a forward declaration, it will result in a shallow object. You can use this object in calls to other methods or constructors and they will validate as expected.

Sample: proxy_test.cc.

Multhreading

When using multi-threaded code in native, it's important to remember the difference between maintaining thread safety in your native code vs your Java code. JNI Bind only handles thread safety of your native handles and class ids. E.g. you might safely pass a jobject from one native thread to another (i.e. you managed to get the handle correctly to the other thread), however, your Java code may not be expecting calls from multiple threads.

To pass a jobject from one thread to another you must use jni::GlobalObject (using jni::LocalObject is undefined).

Upon spinning a new native thread (that isn't the main thread), you must declare a jni::ThreadGuard to explicitly announce to JNI the existence of this thread. It's permissible to to have nested jni::ThreadGuards.

Sample jvm_test.cc.

Overloads

Methods can be overloaded just like in regular Java by declaring a name, and then a variadic pack of jni::Overload. Overloaded methods are invoked like regular methods. JNI Bind will correctly differentiate between functions that differ only by type, including functions that take different class types.

static constexpr Class kClass{
    "com/google/SupportsStrings",
    Method{
        "Foo",
        Overload{jni::Return<void>{}, Params{jint{}}},
        Overload{jni::Return<void>{}, Params{jstring{}}},
        Overload{jni::Return<void>{}, Params{jstring{}, jstring{}}},
    }
};

LocalObject<kClass> obj{};
obj("Foo", 1);
obj("Foo", "arg");
obj("Foo", "arg", jstring{nullptr});

Sample method_test_jni.cc, MethodTest.java.

Class Loaders

Class loaders are a powerful but complex tool that enable loading class definitions at runtime from classes not present in the primordial loader. Their scope is well in excess of this documentation, but you will need a solid understanding of their usage to deploy them successfully (check out the reference links).

To define a class loaded class, you must first define a ClassLoader:

static constexpr ClassLoader kTestClassLoader{
    kDefaultClassLoader, SupportedClassSet{kClassLoaderHelperClass}};

The first argument is the "parent" loader, which can be either kDefaultClassLoader, kNullClassLoader or a different ClassLoader instance you have defined. I recommend reading Baeldung's "Class loaders in Java", but, a rough explanation is that the DefaultLoader supports everything, the NullLoader will support nothing, and a custom loader will support its own classes. When building classes using a loader, your hierarchy will be searched from base to parent searching for the first viable loader.

The second argument is the SupportedClassSet, a set of all classes this loader will directly support building. It is acceptable to support building the same class at multiple layers in your loader hierarchy.

To build objects with a ClassLoader you create a LocalClassLoader or GlobalClassLoader instance. Unfortunately, these currently must be constructed in Java like in the sample (this is deficiency of JNI Bind, feel free to file a bug if you actually need this from native).

LocalClassLoader<kTestClassLoader> class_loader{jclass_loader_obj};
jni::LocalObject<kClassLoaderHelperClass, kTestClassLoader> = class_loader.BuildLocalObject<kClassLoaderHelperClass>();

⚠️ You must add the kTestClassLoader above, JNI Bind incorrectly does not enforce this, but it will be a compilation error in the future, and failing to do so may cause crashes.

Classloaders support building their own instances of classes, and if they can't build it themselves, they will defer to their parents. JNI Bind will validate any class you attempt to build by traversing its hierarchy and resolving to the base most loader available (or the primordial loader if no custom loader is found). Note that the constructor set of the class will be respected, so pass these arguments to Build[Local|Global]Object as if you constructed the object directly.

You can now use this object as usual. This additional decoration may seem excessive, however, this is actually required, because objects of different class loaders are not interchangeable, and they require completely independent class and method lookups.

Sample class_loader_test_jni.cc, ClassLoaderTest.java.

Intro to classloaders, Oracle's intro to classloaders, Baeldung's "Class loaders in Java".

Statics

Statics are declared as an instance of jni::Static which is constructed with jni::Method and jni::Field instances (in that order). Unlike regular objects, you make the static invocation with StaticRef.

static constexpr Class kSomeClass{"com/google/SomeClass"};

static constexpr Class kClass{
  "com/google/HasStaticMethods",
  Static {
    Method { "staticTakesInt", Return{}, Params<int>{}  },
    Method { "staticTakesFloat", Return{}, Params<float>{} },
    Field { "staticLongField", jlong{} },
    Field { "staticObjectField", kSomeClass },
  },
  // Some other field (comes after).
  Field { "Foo", jint{}, }
};

StaticRef<kClass>{}("staticTakesInt", 123);
StaticRef<kClass>{}("staticTakesFloat", 123.f);
StaticRef<kClass>{}["staticLongField"].Set(123);
StaticRef<kClass>{}["staticObjectField"].Set(LocalObject<kSomeClass>{});

Statics will follow the rules laid out in Type Conversion Rules. Invalid static method names won't compile, and jmethodIDs are cached on your behalf. Static method lookups are compile time, there is no hash lookup cost.

Sample static_test_jni.cc, StaticTest.java.

Some classes expose a builder style pattern like so:

  // Class:
  public static class Builder {
    private int valOne = 0;
    private int valTwo = 0;

    public Builder() {}
    public SomeObj build() { return new SomeObj(valOne, valTwo); }

    public Builder setOne(int val) {
      valOne = val;
      return this;
    }

    public Builder setTwo(int val) {
      valTwo = val;
      return this;
    }
  }

  // Usage:
  SomeObj b = new Builder().setOne(123).setTwo(456);

In these circumstances, it is not viable to simply use forward declarations, because the return value will lack the function declarations needed to continue building. To circumvent this, you can use Self, which will return a fully decorated class object with its own class definition.

constexpr Class kBuilder {
    "com/jnibind/test/BuilderTest$Builder",

    Constructor<>{},

    Method{"setOne", Return{Self{}}, Params<int>{}},
    Method{"setTwo", Return{Self{}}, Params<int>{}},
    // FAILS:
    // Method{"setTwo", Return{Class{"com/jnibind/test/BuilderTest$Builder"}}, Params<int>{}},
    Method{"build", Return{kObjectTestHelperClass}},
};

 LocalObject<kBuilder>{}("setOne", 111)("setTwo", 222)("build")

The commented line above would fail to compile at the invocation, because the return value of setTwo is a shallow copy. Self preserves the methods and fields so the returned LocalObject "just works".

Sample: BuilderTest.java, builder_test_jni.cc.

Arrays

Java arrays can be defined in JNI Bind and offer similar mechanics as you would expect with other types.

jni:Array<type, possible_class_definition> provides the static definition, which is referenced by LocalArray<type, possible_class_definition> which provides Get/Set methods, or ArrayView via Pin(bool copy_on_completion=true). ArrayView has a ptr() method which itself can be used to modify underlying values (to be copied back out when ArrayView falls off scope).

LocalArray<jint> arr{3}; // {0, 0, 0}
// LocalArray<jint> int_arr{1,2,3}; // coming soon
LocalArray<jint> another_valid_arr {some_jarray_val};

{
  // Value is written when it falls off scope
  ArrayView view = arr.Pin(/*copy_on_completion=true*/);
  view.ptr()[0] = 123;
}

// You can also use helper methods Set/Get.
arr.Get(0); // 123
arr.Set(1, 456);

If copy_on_completion is false, values will not be copied back when the scope of ArrayView falls off scope (otherwise it will). This can be used as an optimisation when you only intend to read from the array.

Arrays can be used in conjunction with fields and methods as you would expect:

  static constexpr Class kClass{
    "com/google/SupportsStrings",
    Method{ "Foo", Array{int{}} },
    Field { "intArrayField", Array<int>{} },
    // , Field { "intArrayArrayField", Array{ Array { int{} } }  // coming soon
};

LocalObject<kClass> obj{};
LocalArray<int> arr = obj["intArrayField"];

Arrays can be used in conjunction with primitive types or classes, but if they are used with classes they will always consist of LocalObjects (reasoning about a LocalArray with a GlobalObject is too confusing).

static constexpr Class kClass{
    "com/google/SupportsStrings",
    Field { "objectArrayField", Array<jobject, kClass>{} }
};

LocalArray<jint> int_arr{1,2,3};

// Arrays work just like any other type in JNI Bind.
obj("Foo", 1);
obj("Foo", "arg");
obj("Foo", "arg", jstring{nullptr});

LocalArray has two constructors, one for construction from an existing jarray object (similar to LocalObject) and another that builds a new array full of zero initialised objects. For primitives, simply indicate the size, for object arrays, provide a default object that will be used to fill the array.

    LocalArray<int> local_int_array{3};
    LocalArray<jobject, kClass> local_obj_array{5, LocalObject<kClass>{}};

Arrays of arrays, while legal, are not currently supported. They will be supported in the future.

Sample local_array.h, array_test_jni.cc, ArrayTest.java.

Upcoming Features

Feature requests are welcome! Some upcoming features are:

  • Statics
  • Tighter array type validation (arrays are overly permissive for arguments)
  • Better error messages
  • Link time symbol validation in Bazel
  • Unit Testing Support (enabling unit testing of JNI interfaces)
  • Callback syntactic sugar
  • Per JNI call lambda invocations (e.g. per JNI call logging, perf tracing)
  • Auto generated interfaces (with pre-loaded scrapes of Java libraries)
  • And more!

License

The JNI Bind library is licensed under the terms of the Apache license. See LICENSE for more information.

Footnotes

  1. Note, if you pass a jni::LocalObject or jni::GlobalObjectas an rvalue, it will release the underlying jobject (mimicking the same rules used for const& in C++).

jni-bind's People

Contributors

acozzette avatar jwhpryor avatar wcn3 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

jni-bind's Issues

Lacking basic error handling

Hi,

I've noticed that there is currently no error handling at all; No JNI'sExceptionCheck and not even nullptr checks. This leads to app crash when something goes wrong.
For example, trying to invoke a method from a Java class which doesn't exist results in SIGABRT.

This doesn't sound right, and also in contradiction to Android's JNI Tips.

I was wondering if this is something that is going to be fixed in the near future, and also if you have any workaround that can make this (very promising) library usable at its current version.

Thanks in advance!

Resolve compiler issues for common compiler configurations

Some common compilers (such as those bundled with the Android NDK on stock Bazel) have outdated or non-conformant clang implementations.

Some of these legal but unsupported syntaxes could be obviated with equivalent but supported implementations.

Classloader documentation

When release 1.0 is cut there should be no further changes to this API.

For a while I considered having a "LoadedBy" constraint for methods and returns, however, this might be too complicated.

Either way, the syntax needs to be finished as well as the documentation.

Support non-clang consumers

Currently JNI Bind relies on a clang extension (see invocable_map.h).

InvocableMap should support either compiling with this extension, or compiling with C++20 string literals.

This may force a small change in the calling convention for methods and fields, so this must be done before the initial release to prevent introducing legacy or dual APIs for invocation.

In the future, this will also enable non-JNI consumers.

Attempt to remove non-JNI local reference with GlobalObject

When using jni-bind with Android Studio, I am getting a warning in my logs. Looks like:

image

Minimal repro:

JNIEXPORT void JNICALL
Java_com_example_fssrv_MainActivity_sendGreeting(JNIEnv *, jobject, jstring jmsg,
                                                 jobject jcb) {
    static constexpr jni::Class kGreetingCallback{
            "com/example/fssrv/GreetingCallback",
            jni::Method{"invoke", jni::Return<void>{}, jni::Params<jstring>{}}
    };
    jni::LocalString msg(jmsg);
    // absent this Release() below there are two "Attempt to remove non-JNI local..." warnings
    msg.Release();
    jni::GlobalObject<kGreetingCallback> cbo{jni::PromoteToGlobal{}, jcb};
    //cbo.Release();   // <-- doesn't matter Release() here or not
}

My real code is basically replicating the pattern in this test case. Meaning, whether one was to stick a new() on that GlobalObject construction makes no difference.

Checking the Interwebs I found this comment. "If you're a developer getting this message in your own app, make sure you're not accidentally deleting local references given as parameters to your JNI methods."

The jni-bind README states:

Because jobject does not uniquely identify its underlying storage, it is presumed to always be local

While the object always be a local, like the LocalString in the minimal example above, it isn't necessarily our local on which we can do an env->DeleteLocalRef(). I suspect but can't prove that is what is happening with jobject jcb above. Or, at the very least, if I comment out the one line promoting jcb to global, the warning disappears.

The code appears to function well enough with the warning, and indeed I didn't notice until I started porting my Java language binding from Desktop to Android. It is tempting to just ignore it, but this is a protocol thing so there are going to be hundreds or thousands a second.

Add continuous integration

Currently there is extensive CI before port to GitHub, but there is non once it's shuttled.

Non-trivial transformations and discrepancies in Bazel can result in breakage. Also, having build badges helps new users have more confidence that the library is being maintained and works for their intended use case.

Getting compilation errors when using modules

Hi!

I am getting the following compilation error when using certain part of your package inside a module;

Relevant Code:
selenium_bindings.h++

module;

#include <string>

#include <jni.h>
#include <jni_bind.h>

using namespace std;
using namespace jni;

export module selenium_bindings;

export constexpr Class FirefoxDriver {
    "org/openqa/selenium/firefox/FirefoxDriver",
    Constructor()
 };

export class NPC {
private:
    LocalObject<FirefoxDriver> ffxDriver;

public:
    NPC() {
    }

    virtual ~NPC() {
    }
};

main.cc

// inside main
NPC npc("", "");

getting the following error:

/nix/store/9ysy4d521yfhg7x81fkw0vq2kaqcyix5-jnibind-0.9.8/include/jni_bind.h:2377:25: error: 'jni::metaprogramming::StringAsType::static_chars' from module 'selenium_bindings.<global>' is not present in definition of 'StringAsType<chars...>' provided earlier
  static constexpr char static_chars[] = {chars..., 0};
                        ^
/nix/store/9ysy4d521yfhg7x81fkw0vq2kaqcyix5-jnibind-0.9.8/include/jni_bind.h:2377:25: note: declaration of 'static_chars' does not match
  static constexpr char static_chars[] = {chars..., 0};
                        ^
1 error generated.

using:

clang version 16.0.6
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /nix/store/dl48w741h1pbpyzyd4aaq18pr2hvv00y-clang-16.0.6/bin

Add "Performance Mode"

Provide library wide constexpr flags to enable high performance API options.

For example, currently local objects will explicitly release themselves which is required for tests to guarantee their correctness. It also prevents memory leaks (e.g. if a local object was created in a tight loop). This should be configurable.

Why jni-bind requires to specify default (parameterless) constructor if you specify your custom constructor?

Hi.

I don't get it, looks that if I specify my custom constructor, then jni-bind requires to also declare parameter-less constructor, and documentation says so:

If you omit a constructor the default constructor is called, but if any are specified you must explicitly define a no argument constructor.

    static constexpr jni::Class kClass
    {
        "some/class/name",
        jni::Constructor {jstring{}, jint{}, jboolean{}},
        jni::Constructor {}, // <=== Doesn't compile without that. 
    };

But why? What if my class doesn't have a parameterless constructor in Java?

Support native code only usage of JVM

Here is what I have:

HelloWorld.java

package com.test;

public class HelloWorld {

    public void sayHello(String msg) {
        System.out.println("Hello from Java: " + msg);
    }
}

Compile it:

$ javac com/test/HelloWorld.java

The Google JNI-BIND C++ code:

#include "jni_bind_release.h"


// 1: Setup JNI Bind in your OnLoad call (needed only once).
std::unique_ptr < jni::JvmRef < jni::kDefaultJvm >> jvm;

extern "C" {
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * pjvm, void * ) {
        jvm.reset(new jni::JvmRef < jni::kDefaultJvm > (pjvm));
        return JNI_VERSION_1_6;
    }
}

int main() {

    // 2: Define the class
    static constexpr jni::Class kClass {
        "com.test.HelloWorld",
        jni::Method {
            "sayHello",
            jni::Return < void > {},
            jni::Params < jstring > {}
        }
    };

    // 3: Use the class
    jni::LocalObject < kClass > obj {}; // Constructs new instance.

    obj("sayHello", "a cool string from C++");

    return 0;
}

I have the JNI-BIND sources inside a local jni_bind folder. So to compile the code above I do on a Mac:

$ tree -L 1
.
├── HelloWorld.cpp
├── com
└── jni_bind

$ tree com
com
└── test
    ├── HelloWorld.class
    └── HelloWorld.java

C++ compilation:

$ clang++ -std=c++17 -I./jni_bind -I"$JAVA_HOME/include"
-I"$JAVA_HOME/include/darwin/" HelloWorld.cpp -o HelloWorld

And it compiles fine, great!

But now when I run it, I get a segmentation fault because it is not able to find the ./com/test/HelloWorld.class file to load.

$ ./HelloWorld
zsh: segmentation fault  ./HelloWorld

Setting the CLASSPATH does not work either:

$ CLASSPATH=. ./HelloWorld
zsh: segmentation fault  CLASSPATH=. ./HelloWorld

Perhaps I have to set JavaVMOption with -Djava.class.path=. to pass to its embedded JVM? But how would I go about doing that?

☂️ Release 1.0

This bug will be an umbrella work for tracking outstanding work before a full formal 1.0 release is made.

Better compiler errors

JNI Bind fails to compile (by design) when invalid arguments are passed, or names are used. While this is helpful to avoid making mistakes, the error messages are incomprehensible.

When an invalid method is called, and the name is incorrect, something like the following should be emitted in the compiler error for faster scanning:


  • Jni Bind Failure

Method Foo does not exist.

The following signatures are available:
Fooz ( int , int ) ;
Fooz ( float , int ) ;


This will not be completed for Release 1.0.

Godbolt Sample

I would like to add a Godbolt sample so that folks can try JNI Bind directly in their browser.

Add Mac test runner

Mac currently has issues running tests that need to be solved by migrating to at least Bazel 7.0.1.

Code currently compiles and runs, but java tests fail because of bazelbuild/bazel#16421.

The most straightforward fix is to migrate to Bazel 7.0.1, so for now this is blocked by #253.

Generate release header is freezing on macOS

Hi,

When i try run "" it is freezing or stopping somewhere and nothing happen.

paulo ~/Developer/workspaces/cpp  $ git clone https://github.com/google/jni-bind.git
Cloning into 'jni-bind'...
remote: Enumerating objects: 2983, done.
remote: Counting objects: 100% (684/684), done.
remote: Compressing objects: 100% (226/226), done.
remote: Total 2983 (delta 474), reused 511 (delta 425), pack-reused 2299
Receiving objects: 100% (2983/2983), 1.12 MiB | 1.11 MiB/s, done.
Resolving deltas: 100% (2174/2174), done.
paulo ~/Developer/workspaces/cpp  $ cd jni-bind 
paulo ~/Developer/workspaces/cpp/jni-bind [main] $ ./build_jni_bind_release.sh     <--- it freeze here and do nothing

Im using macOS Ventura with M1.

Thanks.

jni_mh.h with jint as 32-bit long

I got jni-bind working on Windows but needed to work-around a difference in how Oracle JDK 19 defines jint.

On Linux:

typedef int jint;

On Windows (comment from the JDK preserved):

// 'long' is always 32 bit on windows so this matches what jdk expects
typedef long jint;

If I compile on Windows, I get TMP errors:

src/main/c/jni_bind_release.h:507:19: error: static assertion failed due to requirement 'AllUnique_v<void, unsigned char, bool, signed char, short, long, float, long, long long, char, unsigned short, double, std::string, _jstring *, char *, const char *, std::string_view, jni::RefBaseTag<_jstring *>, _jobject *, jni::RefBaseTag<_jobject *>, jni::LoaderTag, jni::Object, jni::Self, _jarray *, jni::RefBaseTag<_jarray *>, jni::ArrayTag<_jarray *>, _jobjectArray *, jni::RefBaseTag<_jobjectArray *>, jni::ArrayTag<_jobjectArray *>, _jintArray *, jni::RefBaseTag<_jintArray *>, jni::ArrayTag<_jintArray *>, _jbooleanArray *, jni::RefBaseTag<_jbooleanArray *>, jni::ArrayTag<_jbooleanArray *>, _jbyteArray *, jni::RefBaseTag<_jbyteArray *>, jni::ArrayTag<_jbyteArray *>, _jcharArray *, jni::RefBaseTag<_jcharArray *>, jni::ArrayTag<_jcharArray *>, _jshortArray *, jni::RefBaseTag<_jshortArray *>, jni::ArrayTag<_jshortArray *>, _jdoubleArray *, jni::RefBaseTag<_jdoubleArray *>, jni::ArrayTag<_jdoubleArray *>, _jfloatArray *, jni::RefBaseTag<_jfloatArray *>, jni::ArrayTag<_jfloatArray *>, _jlongArray *, jni::RefBaseTag<_jlongArray *>, jni::ArrayTag<_jlongArray *>>': FindIdxOfVal only operates on unique sets.
  507 |     static_assert(AllUnique_v<Ts...>,
      |                   ^~~~~~~~~~~~~~~~~~
src/main/c/jni_bind_release.h:513:40: note: in instantiation of template class 'jni::metaprogramming::FindIdxOfVal<jni::IsConvertibleKey<void>>::StaticAssertWrapper<void, unsigned char, bool, signed char, short, long, float, long, long long, char, unsigned short, double, std::string, _jstring *, char *, const char *, std::string_view, jni::RefBaseTag<_jstring *>, _jobject *, jni::RefBaseTag<_jobject *>, jni::LoaderTag, jni::Object, jni::Self, _jarray *, jni::RefBaseTag<_jarray *>, jni::ArrayTag<_jarray *>, _jobjectArray *, jni::RefBaseTag<_jobjectArray *>, jni::ArrayTag<_jobjectArray *>, _jintArray *, jni::RefBaseTag<_jintArray *>, jni::ArrayTag<_jintArray *>, _jbooleanArray *, jni::RefBaseTag<_jbooleanArray *>, jni::ArrayTag<_jbooleanArray *>, _jbyteArray *, jni::RefBaseTag<_jbyteArray *>, jni::ArrayTag<_jbyteArray *>, _jcharArray *, jni::RefBaseTag<_jcharArray *>, jni::ArrayTag<_jcharArray *>, _jshortArray *, jni::RefBaseTag<_jshortArray *>, jni::ArrayTag<_jshortArray *>, _jdoubleArray *, jni::RefBaseTag<_jdoubleArray *>, jni::ArrayTag<_jdoubleArray *>, _jfloatArray *, jni::RefBaseTag<_jfloatArray *>, jni::ArrayTag<_jfloatArray *>, _jlongArray *, jni::RefBaseTag<_jlongArray *>, jni::ArrayTag<_jlongArray *>>' requested here
  513 |   static constexpr std::size_t value = StaticAssertWrapper<Ts...>::value;
      |                                        ^
src/main/c/jni_bind_release.h:518:40: note: in instantiation of static data member 'jni::metaprogramming::FindIdxOfVal<jni::IsConvertibleKey<void>>::value<void, unsigned char, bool, signed char, short, long, float, long, long long, char, unsigned short, double, std::string, _jstring *, char *, const char *, std::string_view, jni::RefBaseTag<_jstring *>, _jobject *, jni::RefBaseTag<_jobject *>, jni::LoaderTag, jni::Object, jni::Self, _jarray *, jni::RefBaseTag<_jarray *>, jni::ArrayTag<_jarray *>, _jobjectArray *, jni::RefBaseTag<_jobjectArray *>, jni::ArrayTag<_jobjectArray *>, _jintArray *, jni::RefBaseTag<_jintArray *>, jni::ArrayTag<_jintArray *>, _jbooleanArray *, jni::RefBaseTag<_jbooleanArray *>, jni::ArrayTag<_jbooleanArray *>, _jbyteArray *, jni::RefBaseTag<_jbyteArray *>, jni::ArrayTag<_jbyteArray *>, _jcharArray *, jni::RefBaseTag<_jcharArray *>, jni::ArrayTag<_jcharArray *>, _jshortArray *, jni::RefBaseTag<_jshortArray *>, jni::ArrayTag<_jshortArray *>, _jdoubleArray *, jni::RefBaseTag<_jdoubleArray *>, jni::ArrayTag<_jdoubleArray *>, _jfloatArray *, jni::RefBaseTag<_jfloatArray *>, jni::ArrayTag<_jfloatArray *>, _jlongArray *, jni::RefBaseTag<_jlongArray *>, jni::ArrayTag<_jlongArray *>>' requested here
  518 |     FindIdxOfVal<Comparator>::template value<Ts...>;
      |                                        ^
src/main/c/jni_bind_release.h:760:26: note: in instantiation of variable template specialization 'jni::metaprogramming::FindIdxOfValWithComparator_idx<jni::IsConvertibleKey<void>, void, unsigned char, bool, signed char, short, long, float, long, long long, char, unsigned short, double, std::string, _jstring *, char *, const char *, std::string_view, jni::RefBaseTag<_jstring *>, _jobject *, jni::RefBaseTag<_jobject *>, jni::LoaderTag, jni::Object, jni::Self, _jarray *, jni::RefBaseTag<_jarray *>, jni::ArrayTag<_jarray *>, _jobjectArray *, jni::RefBaseTag<_jobjectArray *>, jni::ArrayTag<_jobjectArray *>, _jintArray *, jni::RefBaseTag<_jintArray *>, jni::ArrayTag<_jintArray *>, _jbooleanArray *, jni::RefBaseTag<_jbooleanArray *>, jni::ArrayTag<_jbooleanArray *>, _jbyteArray *, jni::RefBaseTag<_jbyteArray *>, jni::ArrayTag<_jbyteArray *>, _jcharArray *, jni::RefBaseTag<_jcharArray *>, jni::ArrayTag<_jcharArray *>, _jshortArray *, jni::RefBaseTag<_jshortArray *>, jni::ArrayTag<_jshortArray *>, _jdoubleArray *, jni::RefBaseTag<_jdoubleArray *>, jni::ArrayTag<_jdoubleArray *>, _jfloatArray *, jni::RefBaseTag<_jfloatArray *>, jni::ArrayTag<_jfloatArray *>, _jlongArray *, jni::RefBaseTag<_jlongArray *>, jni::ArrayTag<_jlongArray *>>' requested here
  760 |       TypeOfNthElement_t<FindIdxOfValWithComparator_idx<Comparator, Keys_...>,
      |                          ^
 ... etc etc

Full gist here.

I'm not above changing the typedef in the JDK, but if it is possible to make jni-bind tolerant of the types encapsulated by jni_md.h that would be even better.

Either way, for my own edification, what I don't entirely grok is why the template specialization is failing to begin with. I don't see it. The jlong type is on Windows is:

typedef __int64 jlong;

There is no actual difference in the types on Windows and Linux. The types are unique (32-bit and 64-bit literals). Yet AllUnique<> fails.

HelloWorld Sample

#127

@jwhpryor

If you would like to file a new issues for "Distribution to third party projects" that would be great
please change the bug name and file it against me

It is known that starting a JVM from native is a work under development at the moment. But starting a JVM and calling some C++ code from this JVM should be very well supported and documented.

I suggest a step-by-step guide, on the front page, to do the following on Linux and/or MacOS:

1. Compile the Java code:

public class HelloWorld {

    public void sayHello(int count, String msg) {
        for(int i = 0; i < count; i++) {
            System.out.println("Hello JNI-Bind! => " + msg);
        }
    }

    private native void callCppThatWillCallSayHello();

    public static void main(String[] args) {
        (new HelloWorld()).callCppThatWillCallSayHello();
    }
}

2. Compile the C++ code:

NOTE: The code below is most probably wrong. I kindly ask the jni-bind author to fix it.

#include "jni_bind_release.h"
#include "HelloWorld.h"

// 1: Setup JNI Bind in your OnLoad call (needed only once).
std::unique_ptr < jni::JvmRef < jni::kDefaultJvm >> jvm;

extern "C" {
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * pjvm, void * ) {
        jvm.reset(new jni::JvmRef < jni::kDefaultJvm > (pjvm));
        return JNI_VERSION_1_6;
    }
}

JNIEXPORT void JNICALL Java_HelloWorld_callCppThatWillCallSayHello
  (JNIEnv * env, jobject obj) {

    static constexpr jni::Class kClass {
        "HelloWorld",
        jni::Method {
            "sayHello",
            jni::Return < void > {},
            jni::Params < jint > {},
            jni::Params < jstring > {}
        }
    };

    const char * cmsg = "Awesome";
    jstring msg = env->NewStringUTF(cmsg.c_str());

    jni::LocalObject < kClass > obj {}; // Constructs new instance.
    obj("sayHello", (jint) 3, msg);

     env->DeleteLocalRef(msg);
}

3. Run the Java code

4. See the Hello World output in your terminal

5. Celebrate 🍾

Add per trace logging

This currently exists, and is demonstrated in the the Godbolt sample, however, it is currently enabled via #define ENABLE_DEBUG_OUTPUT.

JNI Bind should never really on any defines, this should be configured through some type of shared configuration.

This task should also enable the caller to customize what the per call trace will be. This will be needed for per trace call logging anyways, since Android does not even share a mechanism to print a log to output/logcat (e.g. printf does not work).

Support Static Methods

Currently there is no static support for methods. This should be possible to define in the top level definition, and will need its own calling convention as there will be no local object to invoke this against.

e.g.

static constexpr jni::Class kClass {...};
jni::Static::Invoke<kClass>("Foo", ...);

Difficulty expressing a method call

Hello. I am having difficulty expressing a method call. My Kotlin definitions are as follows:

sealed interface Event<T> {
    data class Created<T>(val obj: T) : Event<T>
    data class Updated<T>(val obj: T) : Event<T>
    data class Deleted<T>(val obj: T) : Event<T>
}
interface EventListener<T> {
    fun onEventReceived(event: Event<T>)
}
val callback = object : EventListener<T> {
    override fun onEventReceived(event: Event<T>) {
        trySendBlocking(event)
    }
}

someJNICall(callback)

My C++ code is receiving a jobject eventListenerObj representing the callback defined above, and I would like to invoke its onEventReceived method. So far I've attempted to use the following definitions:

static constexpr jni::Class JavaObject {
    "java/lang/Object",
};

static constexpr jni::Class EventCreated {
    "com/myproject/Event$Created",
    jni::Field { "obj", JavaObject },
};

static constexpr jni::Class EventListener{
    "com/myproject/EventListener",
    jni::Method{"onEventReceived", jni::Return{}, jni::Params{EventCreated}},
};
auto javaEvent = jni::LocalObject<EventCreated> {};
javaEvent["obj"].Set(javaRpcObject);

jni::LocalObject<EventListener> eventListener{eventListenerObj};
eventListener("onEventReceived", javaEvent);

and I am experiencing a runtime error:

java_vm_ext.cc:594] JNI DETECTED ERROR IN APPLICATION: JNI CallVoidMethodV called with pending exception java.lang.NoSuchMethodError: no non-static method "Lcom/myproject/EventListener;.onEventReceived(Lcom/myproject/Event$Created;)V"

Running javap against EventListener.class, I see the following output:

{
  public abstract void onEventReceived(com.myproject.Event<T>);
    descriptor: (Lcom/myproject/Event;)V
    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
    Signature: #8                           // (Lcom/myproject/Event<TT;>;)V
    RuntimeInvisibleParameterAnnotations:
      parameter 0:
        0: #9()
          org.jetbrains.annotations.NotNull
}
Signature: #3                           // <T:Ljava/lang/Object;>Ljava/lang/Object;

What is the proper way to call onEventReceived?

EDIT:

I was able to invoke the method successfully in the following manner:

jclass c = env->GetObjectClass(eventListenerObj);
jmethodID m = env->GetMethodID(c, "onEventReceived", "(Lcom/myproject/Event;)V");
env->CallVoidMethod(eventListenerObj, m, javaEvent.operator _jobject *());

How that translates to this library is still an open question.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.