UE:UPL与JNI调用的最佳实践

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
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!--Unreal Plugin Example-->
<root xmlns:android="http://schemas.android.com/apk/res/android">

</root>

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
2
3
4
5
6
7
8
9
10
11
<androidManifestUpdates>
<!--Permission requests-->
<addPermission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<addPermission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<!--Full-screen support for Android-->
<addElements tag="application">
<meta-data android:name="notch.config" android:value="portrait|landscape"/>
<meta-data android:name="android.notch_support" android:value="true"/>
</addElements>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<gameActivityClassAdditions>
<insert>
public String AndroidThunkJava_GetPackageName()
{
Context context = getApplicationContext();
return context.getPackageName();
}
public String AndroidThunkJava_GetInstalledApkPath()
{
Context context = getApplicationContext();
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo;
try{
appInfo = packageManager.getApplicationInfo(context.getPackageName(),PackageManager.GET_META_DATA);
return appInfo.sourceDir;
}catch (PackageManager.NameNotFoundException e){
return "invalid";
}
}
</insert>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* Engine/Source/Programs/UnrealBuildTool/System/UnrealPluginLanguage.cs#L378 
* <!-- optional additions to the GameActivity imports in GameActivity.java -->
* <gameActivityImportAdditions> </gameActivityImportAdditions>
*
* <!-- optional additions to the GameActivity after imports in GameActivity.java -->
* <gameActivityPostImportAdditions> </gameActivityPostImportAdditions>
*
* <!-- optional additions to the GameActivity class implements in GameActivity.java (end each line with a comma) -->
* <gameActivityImplementsAdditions> </gameActivityImplementsAdditions>
*
* <!-- optional additions to the GameActivity class body in GameActivity.java -->
* <gameActivityClassAdditions> </gameActivityOnClassAdditions>
*
* <!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->
* <gameActivityReadMetadata> </gameActivityReadMetadata>
*
* <!-- optional additions to GameActivity onCreate in GameActivity.java -->
* <gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>
*
* <!-- optional additions to GameActivity onDestroy in GameActivity.java -->
* <gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>
*
* <!-- optional additions to GameActivity onStart in GameActivity.java -->
* <gameActivityOnStartAdditions> </gameActivityOnStartAdditions>
*
* <!-- optional additions to GameActivity onStop in GameActivity.java -->
* <gameActivityOnStopAdditions> </gameActivityOnStopAdditions>
*
* <!-- optional additions to GameActivity onPause in GameActivity.java -->
* <gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>
*
* <!-- optional additions to GameActivity onResume in GameActivity.java -->
* <gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>
*
* <!-- optional additions to GameActivity onNewIntent in GameActivity.java -->
* <gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>
*
* <!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
* <gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>
*/

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
2
3
4
5
6
7
8
9
10
11
// for Android
if (Target.Platform == UnrealTargetPlatform.Android)
{
PrivateDependencyModuleNames.Add("Launch");
AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(ModuleDirectory, "UPL/Android/FGame_Android_UPL.xml"));
}
// for IOS
if (Target.Platform == UnrealTargetPlatform.IOS)
{
AdditionalPropertiesForReceipt.Add("IOSPlugin",Path.Combine(ModuleDirectory,"UPL/IOS/FGame_IOS_UPL.xml"));
}

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
2
3
public class GameActivity {
public static native String SingnatureTester();
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class GameActivity */

#ifndef _Included_GameActivity
#define _Included_GameActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_GameActivity_SingnatureTester
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

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
2
3
public class GameActivity {
public static native String SingnatureTester(int ival,double dval,String str);
}

Its signature then becomes:

1
2
3
4
5
/*
* Class: GameActivity
* Method: SingnatureTester
* Signature: (IDLjava/lang/String;)Ljava/lang/String;
*/

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:

  1. Starts with L
  2. Ends with ;
  3. 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
2
3
4
package ue4game;
public class GameActivity {
public static native String SingnatureTester(GameActivity activity);
}

The signature would then be:

1
2
3
4
5
6
7
/*
* Class: ue4game_GameActivity
* Method: SingnatureTester
* Signature: (Lue4game/GameActivity;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_ue4game_GameActivity_SingnatureTester
(JNIEnv *, jclass, jobject);

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public native int nativeGetCPUFamily();
public native boolean nativeSupportsNEON();
public native void nativeSetAffinityInfo(boolean bEnableAffinity, int bigCoreMask, int littleCoreMask);
public native void nativeSetConfigRulesVariables(String[] KeyValuePairs);
public native boolean nativeIsShippingBuild();
public native void nativeSetAndroidStartupState(boolean bDebuggerAttached);
public native void nativeSetGlobalActivity(boolean bUseExternalFilesDir, boolean bPublicLogFiles, String internalFilePath, String externalFilePath, boolean bOBBInAPK, String APKPath);
public native void nativeSetObbFilePaths(String OBBMainFilePath, String OBBPatchFilePath);
public native void nativeSetWindowInfo(boolean bIsPortrait, int DepthBufferPreference);
public native void nativeSetObbInfo(String ProjectName, String PackageName, int Version, int PatchVersion, String AppType);
public native void nativeSetAndroidVersionInformation( String AndroidVersion, String PhoneMake, String PhoneModel, String PhoneBuildNumber, String OSLanguage );
public native void nativeSetSurfaceViewInfo(int width, int height);
public native void nativeSetSafezoneInfo(boolean bIsPortrait, float left, float top, float right, float bottom);
public native void nativeConsoleCommand(String commandString);
public native void nativeVirtualKeyboardChanged(String contents);
public native void nativeVirtualKeyboardResult(boolean update, String contents);
public native void nativeVirtualKeyboardSendKey(int keyCode);
public native void nativeVirtualKeyboardSendTextSelection(String contents, int selStart, int selEnd);
public native void nativeVirtualKeyboardSendSelection(int selStart, int selEnd);
public native void nativeInitHMDs();
public native void nativeResumeMainInit();
public native void nativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data);
public native void nativeGoogleClientConnectCompleted(boolean bSuccess, String accessToken);
public native void nativeVirtualKeyboardShown(int left, int top, int right, int bottom);
public native void nativeVirtualKeyboardVisible(boolean bShown);
public native void nativeOnConfigurationChanged(boolean bPortrait);
public native void nativeOnInitialDownloadStarted();
public native void nativeOnInitialDownloadCompleted();
public native void nativeHandleSensorEvents(float[] tilt, float[] rotation_rate, float[] gravity, float[] acceleration);

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
2
3
4
5
6
7
8
Runtime\Android\AndroidLocalNotification\Private\AndroidLocalNotification.cpp
Runtime\ApplicationCore\Private\Android\AndroidWindow.cpp
Runtime\Core\Private\Android\AndroidPlatformFile.cpp
Runtime\Core\Private\Android\AndroidPlatformMisc.cpp
Runtime\Core\Private\Android\AndroidPlatformProcess.cpp
Runtime\Launch\Private\Android\AndroidEventManager.cpp
Runtime\Launch\Private\Android\AndroidJNI.cpp
Runtime\Launch\Private\Android\LaunchAndroid.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
2
3
4
5
<gameActivityClassAdditions>
<insert>
public native void nativeDoTester(String Msg);
</insert>
</gameActivityClassAdditions>

Then, implement such a function in C++:

1
2
3
4
5
6
7
#if PLATFORM_ANDROID
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeDoTester
(JNIEnv jenv*, jobject thiz, jstring msg);
{

}
#endif

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
2
// Runtime/Core/Public/Android/AndroidPlatform.h
#define JNI_METHOD __attribute__ ((visibility ("default"))) extern "C"

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
2
3
4
5
6
// Runtime/Launch/Public/Android
#include "Android/AndroidJNI.h"
// Runtime/Core/Public/Android
#include "Android/AndroidJavaEnv.h"
// Runtime/Core/Public/Android
#include "Android/AndroidJava.h"

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
2
3
4
5
public String AndroidThunkJava_GetPackageName()
{
Context context = getApplicationContext();
return context.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
2
3
4
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetPackageNameMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName", "()Ljava/lang/String;", false);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace FJavaHelperEx
{
FString FStringFromParam(JNIEnv* Env, jstring JavaString)
{
if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL))
{
return {};
}

const auto chars = Env->GetStringUTFChars(JavaString, 0);
FString ReturnString(UTF8_TO_TCHAR(chars));
Env->ReleaseStringUTFChars(JavaString, chars);
return ReturnString;
}

FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString)
{
FString ReturnString = FStringFromParam(Env, JavaString);

if (Env && JavaString)
{
Env->DeleteLocalRef(JavaString);
}

return ReturnString;

}
}

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:

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE:UPL与JNI调用的最佳实践
Author:LIPENGZHA
Publish Date:2020/11/15 10:45
World Count:7.2k Words
Link:https://en.imzlp.com/posts/27289/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!