I have a system in place that consumes a 3rd party Java API on Android, and passes resulting data to C# using AndroidJavaProxy.
static class FooApi
{
private static readonly AndroidJavaClass Thunks =
new AndroidJavaClass("com.company.plugin.Foo");
private class BarCallback : AndroidJavaProxy
{
private readonly TaskCompletionSource<byte[]> _tcs;
public BarCallback(TaskCompletionSource<byte[]> tcs)
: base("com.company.plugin.BarCallback")
{
_tcs = tcs;
}
public void Resolve(byte[] value)
{
_tcs.SetResult(value);
}
}
public static Task<byte[]> GetBarAsync()
{
var tcs = new TaskCompletionSource<byte[]>();
Thunks.CallStatic("getBar", new BarCallback(tcs));
return tcs.Task;
}
}
In a recent test, I found that 300kb of data takes about 30ms to transfer from the 3rd party API to my Android plugin (C++ to Java). Nice! But, then it takes three minutes to transfer those 300kb from Java to C#:
One of the first considerations is, of course, skipping Java (going straight from C++ to C#). In my situation, however, that’s simply not feasible for multiple reasons.
Clearly, AndroidJavaProxy’s innerworkings are unfortunately inefficient for large payloads. What are my alternatives? What mechanism that allows C# to read data that originated in Java has the least overhead? I’ve implemented systems for sharing data between C# and V8 in various game engines in the past, so I’m not afraid of using unsafe code, but in this situation I have very limited access to information on how Unity has C# interacting with Java.
These ideas come to mind as avenues that are worth pursuing:
Fixed (or pinned via GCHandle) byte[ ] declared in C#. Pointer passed to Java for writing (how? reinterpret cast? marshaling library?).
Java’s-equivalent-of-fixed/pinned Byte[ ] declared in Java. Pointer passed to C# for reading with Marshal.PtrToStructure.
I’d start by trying to avoid any data copying Java ↔ C# in the first place. Given third party Java plugin I’d look for possibility to write it all in Java and only use C# code for orchestration.
Hard to advise without knowing the use case.
The use-case shouldn’t matter, which is part of why I framed my question generically (there are also non-disclosure agreements involved). This makes the discussion more useful for others who have the same problem, but with different use cases. Keeping everything in Java is not an option, unless most of the UnityEngine API is available there.
To be honest, I’m surprised and disappointed to get such a non-answer from Unity staff. Do you have a post quota that you need to meet? If you’re not an expert in Mono/JVM marshaling, then I believe the most useful contribution you can offer would be to refer somebody who is familiar with that area of your product’s source code.
Further, three minutes to copy 300kb of data is absurd. There’s clearly something wrong that’s worth addressing.
I did some experimentation today. Here’s what I found:
class TestA : AndroidJavaProxy
{
public TestA(): base("com.company.plugin.TestA"){}
void getResult(byte[] x)
{
Debug.Log(Encoding.ASCII.GetString(x));
}
}
class TestB : AndroidJavaProxy
{
public TestB() : base("com.company.plugin.TestB") {}
void getResult(AndroidJavaObject x)
{
Debug.Log(Encoding.ASCII.GetString(x.Get<byte[]>("x")));
x.Dispose();
}
}
On Average, TestA took about 130ms to read 100 bytes of data. On the other hand, TestB, despite using a totally unnecessary object to wrap the result, took about 10ms to read the same amount of data.
It appears that AndroidJavaObject.Get<T> is a far more efficient implementation of transferring data from JVM to Mono than is AndroidJavaProxy + callback method parameter. In fact, I’d go as far as to say that passing array parameters is implemented poorly, and should probably be looked into by Unity’s engine programmers (1ms per byte is a head-scratcher).
For anybody looking to truly maximize performance, it’s also possible to return a direct ByteBuffer, retrieve the “address” field, and cast it to an unsafe byte*. With that approach, I ended up with a byte[100] in a mere 2ms.
I’ve recently improved string marshaling around 2x (will come out in 19.2 IIRC). In older releases it is also more efficient to AndroidJavaObject instead of string.
Sounds like I should give a spin for arrays of primitives as well.
Could you report this as bug?
@Max-Pixel_1 I desperately need an example of how this works, performance is shocking and I have to pass a byte[ ] from Java to C# in Unity, how can I send a ByteBuffer or access the byte[ ] in Java from C# in Unity using byte* ?
For anyone out there who comes across this and needs the example more clear. Calling the array() method on the ByteBuffer provides insanely better performance then passing the byte[ ] directly (3 minutes vs <5ms for a 1MB array). Similar to just wrapping the data in an object
// cs
class Test : AndroidJavaProxy
{
public Test() : base("com.company.plugin.Test") {}
void onResultFast(AndroidJavaObject byteBuffer)
{
sbyte[] data = byteBuffer.Call<sbyte[]>("array");
}
void onResultSlow(byte[] data) { }
}
// java
byte[] array = ...;
testInstance.onResultSlow(array); // 3 minutes for 1MB
testInstance.onResultFast(ByteBuffer.wrap(array)); // < 5ms for 1MB