When developing Android with UE4, it is sometimes necessary to obtain platform-related information or perform platform-related operations. In such cases, it is essential to add Java code in the codebase and call it from C++. Some requirements also necessitate receiving events from the Java side in the game, requiring handling the Java calling C++ process.
This article mainly involves the following sections:
- Adding Java code to UE projects
- Java function signature rules
- Java calling C++ functions
- C++ calling Java functions
How to utilize UE’s UPL features, Java’s signature rules, and implement JNI calls in UE will be detailed in the article.
UPL
UPL stands for Unreal Plugin Language, which is an XML-Based structured language used to intervene in the UE packaging process (such as copying so files/editing AndroidManifest.xml, adding iOS frameworks/operating plist, etc.). This article mainly introduces the usage of UPL in Android, and UPL’s use in iOS was introduced in my previous article UE4 Development Notes: Mac/iOS Edition #UPL in iOS Applications.
To add Java code to a UE project, code needs to be inserted into GameActivity.java during packaging via UPL.
The syntax for UPL uses XML, and the file needs to be saved in .xml
format:
1 |
|
In <root></root>
, you can use the nodes provided by UPL to write logic (but because its syntax is XML-based, writing loops and other control flows can be quite cumbersome). For example, to add permission requests in AndroidManifest.xml
(the following code is all within <root></root>
):
1 | <androidManifestUpdates> |
Using the androidManifestUpdates
node, you can update the AndroidManifest.xml
. UPL provides many platform-related nodes for both iOS and Android, and it is essential to note that these cannot be mixed.
UPL also provides a node for adding Java methods to the GameActivity class: gameActivityClassAdditions
. Through this node, you can write Java code directly in UPL, and during Android package building, this code will be automatically inserted into the GameActivity
class in GameActivity.java
:
1 | <gameActivityClassAdditions> |
After insertion, the generated file:
These two functions are now in GameActivity.java
. UPL has many nodes for adding content to GameActivity, but this information is not fully documented in UE; for specifics, you still need to refer to the UBT code: UnrealBuildTool/System/UnrealPluginLanguage.cs#L378.
UPL supports the extension of GameActivity. Not only can you add functions, but you can also add additional code to functions like OnCreate
/OnDestroy
, making it easier to intervene at different times as needed.
1 | /* Engine/Source/Programs/UnrealBuildTool/System/UnrealPluginLanguage.cs#L378 |
So, after writing the UPL script, how do you use it?
You need to add the following code to the build.cs
of the Module that requires this UPL:
1 | // for Android |
You specify our UPL script using AdditionalPropertiesForReceipt
. Note that AndroidPlugin
and IOSPlugin
should not be modified, and the file path can be specified based on the location of the UPL file in the project.
Using this method, UPL is added to UE’s build system, and the logic in our script will be automatically executed when building for Android/iOS platforms.
Java Function Signatures
What is JNI? JNI stands for Java Native Interface and is mainly used to call code from other languages from Java and vice versa.
In the previous section, we added Java code to GameActivity via UPL. To call these Java functions from C++ in UE, JNI calls need to be utilized.
To call Java from C++, you first need to know the signature of the Java function you want to call. A signature describes the parameter types and return type of a function.
For example, with the following function:
1 | public String AndroidThunkJava_GetPackageName(){ return ""; } |
It accepts no parameters and returns a Java String value. Therefore, what is its signature?
1 | ()Ljava/lang/String; |
The calculation of the signature follows specific rules, which will be detailed later.
The javac
provided by the JDK has an option that allows generating C++ header files from Java code, facilitating JNI calls, including the signature.
Here’s a test Java code to generate the JNI call header:
1 | public class GameActivity { |
The generation command is:
1 | javac -h . GameActivity.java |
This will generate .class
and .h
files in the current directory. The contents of the .h
file are as follows:
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
This exports the signature information for the SingnatureTester
JNI call member of the GameActivity
class, which is included in the comment: ()Ljava/lang/String;
.
Java_ue4game_GameActivity_SingnatureTester
is the function name that can be implemented in C/C++. When we implement a function with this name in C++, calling GameActivity
‘s SingnatureTester
from Java will invoke our C++ implementation.
Now, if we change the function declaration to:
1 | public class GameActivity { |
Its signature then becomes:
1 | /* |
From the two examples above, we can see the signature rules for Java functions: a signature consists of two parts—parameters and return values.
The ()
in the signature refers to the parameter type signature arranged in order, and the signature after ()
refers to the return value type signature.
What are the rules for Java type signatures? You can refer to the following Java signature comparison table: JNI Signature Comparison Table.
Basic types and their signature comparison table in Java:
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
According to the above rules, the signature for void EmptyFunc(int)
would be (I)V
.
The signature rules for non-internal basic types are:
- Starts with
L
- Ends with
;
- Class name and package are separated by
/
For instance, class types in Java:
- String:
Ljava/lang/String;
- Object:
Ljava/lang/Object;
Testing the example with a package:
1 | package ue4game; |
The signature would then be:
1 | /* |
JNI: Java to C++
Many native
functions declared in the GameActivity
generated by UE are implemented in C++. These functions, when executed in Java, will automatically invoke the C++ code within the engine:
1 | public native int nativeGetCPUFamily(); |
In the previous section, Java signatures have already been mentioned. The native
methods allow Java to call implementations in other languages. These functions are implemented in UE, designed to handle different logics for Android devices, defined in the following files:
1 | Runtime\Android\AndroidLocalNotification\Private\AndroidLocalNotification.cpp |
We can also add native
functions to the GameActivity ourselves. If some SDKs provide functions like native
, we can implement them using the following method. Here is a simple example showing how to add a native
function to GameActivity via UPL and implement it on the C++ side.
1 | <gameActivityClassAdditions> |
Then, implement such a function in C++:
1 |
|
com.epicgames.ue4
is the package name of the generated GameActivity.java
(package com.epicgames.ue4;
).
As you can see, the convention for naming the function implemented in C++ with the JNIMETHOD is:
1 | RType Java_PACKAGENAME_CLASSNAME_FUNCNAME(JNIEnv*, jobject thiz, Other...) |
Note: This function must be a C function and should not participate in C++ name mangling; otherwise, the signature will be incorrect.
In UE, you can use the JNI_METHOD
macro defined in AndroidPlatform.h
.
1 | // Runtime/Core/Public/Android/AndroidPlatform.h |
You can also use extern "C"
. After defining this in C++, if the Java side calls the function, it will execute the logic we wrote in C++.
JNI: C++ to Java
From the previous section, you now understand the signature information for Java functions. So how do you call Java code from C++ in UE using the function name and signature information?
UE encapsulates a large number of JNI helper functions on the C++ side, making it convenient to perform JNI operations. These functions are mostly defined in the following three header files:
1 | // Runtime/Launch/Public/Android |
Since AndroidJNI.h
is in the Launch
module, you need to add that module to the Build.cs for the Android
platform.
Using an example function we added to the GameActivity class via UPL from the first section:
1 | public String AndroidThunkJava_GetPackageName() |
To call it from UE, you first need to obtain its jmethodID
, which is required based on the class the function belongs to, function name, and signature:
1 | if (JNIEnv* Env = FAndroidApplication::GetJavaEnv()) |
Since our code is inserted into the GameActivity
class, and UE has encapsulated GameActivity
, we can use FJavaWrapper
to obtain it. FJavaWrapper
is defined in Runtime/Launch/Public/Android
.
The obtained methodID
is somewhat similar to a member function pointer in C++. To invoke it, you need to execute a call on some object. UE also provides encapsulation for this:
1 | jstring JstringResult = (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis, GetPackageNameMethodID); |
By using CallObjectMethod
, you invoke GetPackageNameMethodID
on the instance of GameActivity
. The returned value is a Java object, which still cannot be directly converted into a UE string; it requires a conversion process:
1 | namespace FJavaHelperEx |
By using the defined FJavaHelperEx::FStringFromLocalRef
, a jstring
can be converted to a UE FString:
1 | FString FinalResult = FJavaHelperEx::FStringFromLocalRef(Env, JstringResult); |
At this point, the entire JNI call process is complete, and you can call Java from C++ and obtain return values.
Conclusion
References: