Unity Native: Android Native Calls
By Artem Glotov
In this article, we delve into the subtleties of making native calls in Android bindings, the process of data transfer between managed and native code, the optimal methods for transferring complex types, and the differences between invoking Java and Kotlin code.
Before proceeding, familiarize yourself with the official documentation* to grasp the basics, as this article primarily focuses on elaborating on details not covered there.
Supported Types
The table below details the types and typed arrays that Unity’s high-level API — AndroidJavaObject
, AndroidJavaClass
, and AndroidJavaProxy
— supports out of the box. This means you can use these types in the parameters and return types of your methods, and Unity will handle the conversion to the respective Java types automatically. For cases outside of this inherent support, the use of AndroidJavaObject
is necessary.
Description | C# | Java / Kotlin | Support for Type | Support for Typed Array |
---|---|---|---|---|
8-bit Signed Integer | sbyte | byte / Byte | Yes | Yes |
16-bit Signed Integer | short | short / Short | Yes | Yes |
32-bit Signed Integer | int | int / Int | Yes | Yes |
64-bit Signed Integer | long | long / Long | Yes | Yes |
32-bit Floating-Point Number | float | float / Float | Yes | Yes |
64-bit Floating-Point Number | double | double / Double | Yes | Yes |
16-bit Unicode Character | char | char / Char | Yes | Yes |
Boolean | bool | boolean / Boolean | Yes | Yes |
String | string | String / String | Yes | Yes |
8-bit Unsigned Integer | byte | – | Partially (will be casted to sbyte with warning) | Partially (will be casted to sbyte[] with warning) |
16-bit Unsigned Integer | ushort | – | No (return default value or throw exception) | No (throw exception) |
32-bit Unsigned Integer | uint | – | No (return default value or throw exception) | No (throw exception) |
64-bit Unsigned Integer | ulong | – | No (return default value or throw exception) | No (throw exception) |
Value Type (Struct) | struct | – | No | No |
Reference Type (Class) | AndroidJavaObject | any reference type | Yes | Only for returned type |
Example
public class PrimitiveTypesBindings
{
public int PassInt(int value)
{
using (var javaObject = new AndroidJavaObject("com.sample.java.samples.PrimitiveTypesSample"))
{
return javaObject.Call<int>("passInt", value);
}
}
}
public class PrimitiveTypesSample {
public int passInt(int value) {
System.out.println("Int value: " + value);
return value;
}
}
For any reference type, except string
, the use of AndroidJavaObject
is required; otherwise, an exception will be thrown. It’s important to note that for some unsupported primitive types specified as a return type, Unity doesn’t show an error, but instead returns their default value.
Transfer objects
Let’s consider a Java class with two methods: one that takes a complex object as a parameter and another that takes a simple object.
public class DataTransferSample {
public void passComplexData(TestData data) {
System.out.println("Complex data: " + data);
}
public void passSimpleData(TestItem data) {
System.out.println("Simple data: " + data);
}
}
There are two common ways to pass objects to Java:
- Using
AndroidJavaObject
to create a Java object on the managed side and pass it to Java
- Using serialization to convert a managed object into a string and deserialize it back on the Java side. This approach can only be used if you’re writing the Android Library yourself or if you’re creating a wrapper for an existing Android Library.
Wrapper Example
public class DataTransferSampleWrapper {
private final DataTransferSample sample;
private final Gson gson = new Gson();
public DataTransferSampleWrapper(DataTransferSample sample) {
this.sample = sample;
}
public void passComplexSerializedData(String serializedData) {
TestData data = gson.fromJson(serializedData, TestData.class);
sample.passComplexData(data);
}
public void passSimpleSerializedData(String serializedData) {
TestItem data = gson.fromJson(serializedData, TestItem.class);
sample.passSimpleData(data);
}
}
Performance tests were conducted to evaluate the speed at which Unity can transfer data to native code using the following methods:
- AndroidJavaObject: Utilizes
AndroidJavaObject
on the managed side to transfer data to native - NewtonsoftJson: Employs
Newtonsoft.Json
for serializing data into a string on the managed side, then deserializing it using Gson on the native side - JsonUtility: Uses
JsonUtility
for serializing data into a string on the managed side, then deserializing it using Gson on the native side
For tests I used the following data structures:
Complex object
[System.Serializable]
public class TestData {
public int Id { get; set; }
public string Name { get; set; }
public List<float> Values { get; set; } // x100 items
public Dictionary<string, TestItem> Items { get; set; } // x100 items
}
public class TestData {
public int id;
public String name;
public List<Float> values;
public Map<String, TestItem> items;
}
Simple object
[Serializable]
public class TestItem
{
public long Value { get; set; }
}
public class TestItem {
public long value;
}
Results:
- JsonUtility: This is the quickest method for handling complex data. For small data, it shows average results.
- AndroidJavaObject: This method is fast for small and simple data. But when the data gets big and complex, it becomes the slowest. Also, it uses up more memory compared to other methods, regardless of the size of the data.
- For any approach, the first call is the most expensive. Subsequent calls are much faster due to the caching of the Java method IDs and reflection information used for serialization.
Measured Number of GC allocation Calls
Measured CPU Time in Milliseconds
Generic Types
JVM generics work using type erasure, which allows you to create instances of generic types without needing to specify the generic type from the C# side. In Java, type checks aren’t performed when you create an instance or pass it as an argument; they only occur when you actually use the generic items.
public int PassListOfWrongType(List<float> values, bool useItems)
{
using var arrayList = new AndroidJavaObject("java.util.ArrayList");
using var floatClass = new AndroidJavaClass("java.lang.Float");
foreach (var value in values)
{
using var floatClassValue = floatClass.CallStatic<AndroidJavaObject>("valueOf", value);
arrayList.Call<bool>("add", floatClassValue);
}
return _javaInstance.Call<int>("passStringList", arrayList, useItems);
}
public class GenericTypesSample {
public int passStringList(List<String> list, boolean useItems) {
if (useItems) {
for (String item : list) {
System.out.println(item);
}
}
return list.size();
}
}
Kotlin Key Differences
Many types are similar to those in Java, as you can see in the number representation on the JVM**. However, there are differences when writing Kotlin plugins and bindings for Unity compared to Java. Most of these differences stem from the differences between Kotlin and Java**.
Nullability
Kotlin has a null-safe type system**, so you need to specify whether a type can be null or not. For example, String
is a non-nullable type, but String?
is nullable type.
Example
The following methods looks quite similar in Java and Kotlin:
public class PrimitiveArrayTypesSample {
public byte[] passByteArray(byte[] values) {
System.out.println("Byte array values: " + Arrays.toString(values));
return values;
}
}
class PrimitiveArrayTypesSample {
fun passByteArray(values: ByteArray): ByteArray {
println("Byte array values: " + values.contentToString())
return values
}
}
But the Kotlin example will throw an exception if you pass null
as an argument because ByteArray
is a non-nullable type. The correct way is to use ByteArray?
instead if you expect null
as an argument:
class PrimitiveArrayTypesSample {
fun passByteArray(values: ByteArray?): ByteArray? {
println("Byte array values: " + values.contentToString())
return values
}
}
Singleton (Object)
Kotlin has a concept called an object declaration**. An object
is a class that has only one instance (a singleton). To make an object
‘s method callable like a static method in Java or C#, you need to annotate it with @JvmStatic
. Without this annotation, you must reference the static INSTANCE
property to call the method.
Example
With @JvmStatic
Annotation
public static bool CallStaticMethodFromSingleton()
{
using var singletonClass = new AndroidJavaClass("com.sample.kotlin.samples.SingletonSample");
return singletonClass.CallStatic<bool>("callStaticMethod");
}
object SingletonSample {
@JvmStatic fun callStaticMethod(): Boolean {
println("This is a static method")
return true
}
}
Without @JvmStatic
annotation
public static bool CallInstanceMethodFromSingleton()
{
using var singletonClass = new AndroidJavaClass("com.sample.kotlin.samples.SingletonSample");
using var singletonInstance = singletonClass.CallStatic<AndroidJavaObject>("INSTANCE");
return singletonInstance.Call<bool>("callInstanceMethod");
}
object SingletonSample {
fun callInstanceMethod(): Boolean {
println("This is an instance method")
return true
}
}
Companion Object
One of the differences between Kotlin and Java is that Kotlin does not have explicit static methods or fields. Instead, it uses companion objects to hold methods and properties that are shared by all instances of a class. To make a companion object method callable like a static method from Java or C#, you need to annotate it with @JvmStatic
.
Example
public static bool CallStaticMethod()
{
using var staticClass = new AndroidJavaClass("com.sample.kotlin.samples.StaticMethodSample");
return staticClass.CallStatic<bool>("callStaticMethod");
}
class StaticMethodSample {
companion object {
@JvmStatic fun callStaticMethod(): Boolean {
println("This is a static method")
return true
}
}
}
Unsigned types
Kotlin has experimental support for unsigned types**. However, they aren’t compatible with C# unsigned types for the following reasons:
- Unsigned numbers in Kotlin are implemented as inline classes, having a single storage property that corresponds to the Java signed counterpart of the same width
- Unity doesn’t support unsigned C# classes in
AndroidJavaObject
Summary
Now, you have enhanced your knowledge about how to make calls to Android’s native code, how to transfer simple and complex object types, work with generic Java types from Unity code, and write Java and Kotlin plugins.
(*) Content in this article is sourced from Unity’s Documentation. Copyright © 2024 Unity Technologies. Publication Date: 2024-02-05.
(**) Content in this article is sourced from Kotlin’s Documentation. Copyright © 2024 JetBrains s.r.o.