Jenny is a java annotation processor, which helps you generate C/C++ code for JNI calls according to your java native class.
Jenny comes with two main part:
- NativeGlueGenerator: which generate skeleton C++ code for your native class/method.
- NativeProxyGenerator: which generate helper C++ class for you to call java APIs through JNI interface, including create new instance, call method, get/set fields, define constants.
Glue stands for c++ code to implement Java native method. (Glue java and C++.)
Proxy stands for c++ class to provide calls to java from c++. (c++ side proxy for the java class.)
And there is an extra bonus -- jnihelper.h that uses C++ RAII technology to simplify JNI APIs. When opt-in (with 'jenny.useJniHelper'=true
), the generated proxy class will also add methods using jnihelper
, which makes life even happier!
When writing JNI code, people usually come across APIs where java method/field/type signatures are required, some of them like JNIEnv::RegisterNatives
, JNIEnv::FindClass
, JNIEnv::GetMethodID
, etc. It is very hard to hand-craft those signatures correctly and efficiently, so programmers often waste much time writing and debugging those boilerplate.
Jenny is now your JNI code maid, who takes care of all those boilerplate so you can be even more productive.
Let's see what the generated code is.
You can find full code in sample-gen.
Java class.
@NativeClass
public class NativeTest {
public static final int RUNTIME_TYPE_MAIN = 1;
public native int add(int a, int b);
public native void cpp_magic(String s, byte[] data);
}
The generated Glue code.
// NativeTest.h
namespace NativeTest {
static constexpr auto FULL_CLASS_NAME = u8"io/github/landerlyoung/jennysampleapp/NativeTest";
static constexpr jint RUNTIME_TYPE_MAIN = 1;
jint JNICALL add(JNIEnv* env, jobject thiz, jint a, jint b);
void JNICALL cpp_magic(JNIEnv* env, jobject thiz, jstring s, jbyteArray data);
inline bool registerNativeFunctions(JNIEnv* env) { ... }
}
// NativeTest.cpp
jint NativeTest::add(JNIEnv* env, jobject thiz, jint a, jint b) {
// TODO(jenny): generated method stub.
return 0;
}
void NativeTest::cpp_magic(JNIEnv* env, jobject thiz, jstring s, jbyteArray data) {
// TODO(jenny): generated method stub.
}
Jenny generate:
- constant defines
- JNI register function
- native method declare with the same name as java methods
- native method implementation stubs
You just need to fill the stubs with real code.
The following code is a show case that C++ uses OkHttp to perfomr a HTTP get operation through JNI APIs.
jstring func(jstring _url) {
jenny::LocalRef<jstring> url(_url, false);
OkHttpClientProxy client = OkHttpClientProxy::newInstance();
BuilderProxy builder = BuilderProxy::newInstance().url(url);
RequestProxy request = builder.build();
CallProxy call = client.newCall(request.getThis());
ResponseProxy response = call.execute();
ResponseBodyProxy body = response.body();
return body.string().release();
}
And here is the equivlent java code.
String run(String url) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
If you are femiliar with JNI, you'd be surprised! The C++ code using Jenny just as clean as the Java code. Without Jenny it would be a nightmare.
And here is another real world comparesion using URLConnection
api with vs without jenny. 🔗Follow the link to see the nightmare!
And also, here is another example without jnihelper
.
void NativeDrawable::draw(JNIEnv *env, jobject thiz, jobject _canvas) {
auto bounds = GraphicsProxy::drawableGetBounds(env, thiz);
GraphicsProxy::setColor(env, state->paint, state->color());
GraphicsProxy::drawableCircle(
env, _canvas,
RectProxy::exactCenterX(env, bounds),
RectProxy::exactCenterY(env, bounds),
std::min(RectProxy::exactCenterX(env, bounds),
RectProxy::exactCenterY(env, bounds)) * 0.7f,
state->paint
);
}
Jenny comes with two component
- the annotation library
- the annotation-processor
👈👈👈 click here for latest version on maven central.
dependencies {
compileOnly 'io.github.landerlyoung:jenny-annotation:1.0.0'
kapt 'io.github.landerlyoung:jenny-compiler:1.0.0'
// for non-kotlin project use:
// annotationProcessor 'io.github.landerlyoung:jenny-compiler:1.0.0'
}
For kotlin project, you gonna need the kotlin-kapt
plugin.
That's it!
The generated code directory depends on your compiler config, typically, for android project it's inside build/generated/source/kapt/debug/jenny
, for java project it's build/generated/sources/annotationProcessor/java/main/jenny
. Also, you can use your own directory with simple config, see below.
You can use the generated code as you like, copy-past manually, or use gradle to copy them automatically (see sample in sample-android/guild.gradle
).
Add @NativeClass()
annotation to you native class in order to let Jenny spot you class, and then generate corresponding cpp source.
Then Jenny would generate code for you. sample-gen contains samples for Jenny generated code.
Note: There is a config field in
NativeClass.dynamicRegisterJniMethods
, whentrue
(the default value) will generate code registering JNI function dynamically on the JNI_OnLoad callback byJNIEnv::RegisterNatives
, instead of using JNI function name conversions (what javah/javac does).
Add @NativeProxy
to your normal java/kotlin class, need to cooperate with @NativeMethodProxy
and @NativeFieldProxy
, please read the doc.
Also, you can tell Jenny to generate code for libray classes by using the @NativeProxyForClasses
annotation. (note)
(note): Use this feature with caution. When your compile-class-path and runtime-class-path have different version of the same class, it's easy to crash because the proxy can't find some method which appears in compile-time but not on runtime. For instance to generate proxy for
java.net.http.HttpRequest
compiled with java-11, ran with java-8, your code crashes because that class just don't exist before java-11.In this case, the recommanded way is to write your own helper class, and generate proxy for it.
Jenny annotation processor arguments:
name | default value | meaning |
---|---|---|
jenny.threadSafe |
true |
The proxy class supports lazy init, this flag controls if the lazy init is thread safe or not. |
jenny.errorLoggerFunction |
null |
When proxy failed to find some method/class/field use the given function to do log before abort. The function must be a C++ function on top namespace with signature as void(JNIEnv* env, const char* error) |
jenny.outputDirectory |
null |
By default, Jenny generate filed to apt dst dir, use this argument to control where the generated files are. |
jenny.fusionProxyHeaderName |
jenny_fusion_proxies.h |
The fusionProxyHeader is a header file that include all generated proxy files and gives you a jenny::initAllProxies function to init all proxies at once, this flag changes the file name. |
jenny.headerOnlyProxy |
true |
The generated proxy file use header only fusion or not. |
jenny.useJniHelper |
false |
Turn on/off jnihelper |
And also, there are some config in Jenny's annotations, please read the doc.
- For kotlin project, it simple
kapt {
// pass configurations to jenny
arguments {
arg("jenny.threadSafe", "false")
arg("jenny.errorLoggerFunction", "jennySampleErrorLog")
arg("jenny.outputDirectory", project.buildDir.absolutePath+"/test")
arg("jenny.headerOnlyProxy", "true")
arg("jenny.useJniHelper", "true")
arg("jenny.fusionProxyHeaderName", "JennyFisonProxy.h")
}
}
-
For Android, you can also do this.
-
For Java Project, do this:
compileJava {
options.compilerArgs += [
"-Ajenny.threadSafe=false",
"-Ajenny.useJniHelper=false",
]
}
When using JNI with multi-thread in C++, please be noticed the pure
native thread (that is create in C++ then attached to jvm) has its class loader as the boot class loader, so on such thread you can only see java standard library classes. For more info, please refer to here.
To solve this problem, please init proxy classes on the JNI_OnLoad
callback, and there is a jenny_fusion_proxies.h
may by helpful.
Open sourced under the Apache License Version 2.0.
If you like or are using this project, please start!