This post covers four things:

  • How C# calls Java functions in Unity.
  • How Java calls C++ through JNI.
  • How C++ under JNI calls Java.
  • How Java calls Unity’s C#.

CSharp Call Java

Use Unity’s AndroidJavaObject, and pass the Java class object into the constructor. The format is the package name followed by the class name.

For example, in Java:

package com.example.jni;

public class ExampleJavaClass {
	// Function with no return value
	void TestFuncVoid(int a) { /* ... */ }
	// Function with a return value
	int TestFuncInt(int a) { /* ... */ }
}

In CSharp, write it like this:

_javaObject = new AndroidJavaObject("com.example.jni.ExampleJavaClass");
// Function with no return value
_javaObject.Call("TestFuncVoid", 12345);
// Function with a return value
int result = _javaObject.Call<int>("TestFuncInt", 12345);

Java Call C++

In a Java function signature, native means this function is executed by calling into C++ through JNI. Before calling it, make sure the corresponding .so has already been loaded, which is usually done with System.loadLibrary(). Here is an example.

public class ExampleJavaClass {
	static {
        System.loadLibrary("jni_example");
    }
    
    private native String stringFromJNI();
    private native void dosomethingInJNI(String a, String b);
}

Next, these two functions need to be implemented on the C++ side. There are two ways Java can find the C++ implementation:

Passive Loading

Java finds the C++ function by its C++ function name, so the C++ name must follow certain rules. For the two functions above, the function signatures on the C++ side should be:

extern "C" JNIEXPORT jstring JNICALL  
Java_com_example_jni_ExampleJavaClass_stringFromJNI(JNIEnv *env, jobject thiz);

extern "C" JNIEXPORT void JNICALL  
Java_com_example_jni_ExampleJavaClass_dosomethingInJNI(JNIEnv *env, jobject thiz, jstring a, jstring b);

Things to note:

  • extern "C" is required, otherwise C++ will mangle the function name.
  • In Android Studio, the IDE can generate this signature with one click.

Active Loading

Instead of naming functions according to those rules, you can actively bind the corresponding Java functions when the library is loaded.

JNIEXPORT jstring JNICALL CppStringFromJNI(JNIEnv* pEnv, jobject clazz)  
{  
    /* ... */ 
}

JNIEXPORT void JNICALL CppDosomethingInJNI(JNIEnv* pEnv, jobject thiz, jstring tag, jstring message)
{  
    /* ... */ 
}

jint JNI_OnLoad(JavaVM* pVm, void* reserved)  
{  
    JNIEnv * env;  
    pVm->GetEnv((void**)&env,JNI_VERSION_1_6);  
  
    JNINativeMethod methods[] ={  
            { "stringFromJNI", "()Ljava/lang/String;",(void*)CppStringFromJNI },  
            { "dosomethingInJNI", "(Ljava/lang/String;Ljava/lang/String;)V",(void*)CppDosomethingInJNI },  
    };  
  
    env->RegisterNatives(env->FindClass("com/example/jni/ExampleJavaClass"), methods, 2);  
    return JNI_VERSION_1_6;  
}

JNI_OnLoad is a fixed JNI function that runs when the library is loaded. What we do inside JNI_OnLoad is register the two C++ functions as native functions that Java can call.

In the JNINativeMethod array, the first parameter is the function name on the Java side, the second is the JNI function signature format, and the third is the function pointer on the C++ side.

In this case, you do not need to write extern "C".

C++ Call Java

There are four steps: get the JNIEnv pointer, find the Java class, find the method ID, and call the method. For example, suppose Java looks like this.

package com.example.jni;

public class ExampleJavaClass {
	public static int doSomething(int a, int b) { /* ... */ }
}

In C++, the first thing to solve is how to get the JNIEnv pointer. This pointer is thread-specific. If the current thread context already has one, you can use it directly. If not, you need to attach to the JNI thread through JavaVM and get a JavaEnv pointer.

JNIEnv* pJniEnv;
pJavaVM->AttachCurrentThread(&pJniEnv, nullptr);

If you do not have a JavaVM pointer, cache one ahead of time. Any JNIEnv pointer can directly get the VM.

JavaVM* pVm;
pJniEnv->GetJavaVM(&pVm);

Even if there is nowhere convenient to get the VM through a JNIEnv pointer, you can definitely get the VM pointer inside JNI_OnLoad right after the .so is loaded.

Once you have the JNIEnv pointer, the following steps in C++ look like this:

// Each step should be guarded properly; omitted here.
void DoSomethingInJava(JNIEnv* pEnv, int a, int b)
{
	// Find the Java class
	jclass javaClass = pEnv->FindClass("com.example.jni.ExampleJavaClass");

	// Get the method ID
	jmethodID javaMethod = pEnv->GetMethodID(javaClass, "doSomething", "(II)I");

	// Call the static method
	jint result = pEnv->CallStaticIntMethod(javaClass, javaMethod, a, b);
}

If it is a non-static function, you need one extra jobject, which can either be passed in from Java or created in C++.

jobject javaObj = pEnv->AllocObject(javaClass);

Then, when calling a non-static member function, just use CallxxxMehod without Static.

Java Call CSharp

Generally speaking, there are two ways to do this: one uses Unity’s messaging mechanism, and the other uses Unity’s AndroidJavaProxy interface mechanism.

Unity Messaging Mechanism

On the Java side, you need to import Unity’s package com.unity3d.player.UnityPlayer. Then call UnitySendMessage directly.

UnityPlayer.UnitySendMessage(String objecyName, String methodName, String message);

I do not recommend this approach, because it requires adding one more package to the aar.

AndroidJavaProxy Interface Mechanism

The basic idea is:

  • Declare an interface on the Java side.
  • On the C# side, use Unity’s AndroidJavaProxy to implement a C# class for that interface.
  • Call Java from C# and pass this C# class to the Java side.
  • Then Java can call C# functions through the interface.

For example, first declare an interface on the Java side.

public interface ICSharpInterface {  
    void LogToCSharp(String message);  
}

Then on the C# side, create a class that implements this interface. It says “implements”, but the essence is that the function signatures must match exactly; Unity uses reflection on both sides through AndroidJavaProxy to build the bridge.

public class JavaCSCaller : AndroidJavaProxy
{
	// Pass the interface to implement into the constructor: package name + class name.
    public JavaCSCaller() : base("com.example.jni.ICSharpInterface")
    {
    }

    void LogToCSharp(string message)
    {
        Debug.LogError(message);
    }
}

Then on the Java side, find a class to hold this interface.

public class ExampleJavaClass {
	private static ICSharpInterface _csCaller = null;

	public static setCSharpCaller(ICSharpInterface caller) {
		_csCaller = caller;
	}

	public static logToCSharp(String message) {
		if (_csharpInterface != null)
            _csharpInterface.LogToCSharp(message);
	}
}

After that, create JavaCSCaller on the C# side and set it on Java.

AndroidJavaClass jClass = new AndroidJavaClass("com.example.jni.ExampleJavaClass");
jClass.CallStatic("setCSharpCaller", new JavaCSCaller());

With that in place, calling ExampleJavaClass’s logToCSharp on the Java side will call the implementation on the C# side.

Notes

  • Types in both Java and C# must not be obfuscated. They find each other through reflection, so once names are obfuscated, they may no longer be found.
  • Make sure the game’s minSdk version is higher than the aar’s minSdk version.

Practical Example

UnityJniExample

First, open AndroidAar with Android Studio and build the corresponding aar, then drag it into Unity and import it. Next, drag the CSharp files under GameScripts into the project. In this example:

  • JavaCSCaller is the implementation of the corresponding Java-side interface, and needs to be passed to Java to implement Java calling C#.
  • JavaProxy is the class used to implement CSharp calling Java.
  • DemoMonoBehaviour is a wrapper where you can serialize three buttons to trigger the three flows separately.

The first button returns a string from JNI to Java, then back to C#, and prints it.

The second button passes a string from C# to Java, then Java calls C++ to print it to Logcat.

The third button passes a string from C# to Java, then Java calls C# to print it.