Ok, so following a chat with Google engineering and after reading the code I've reached the following conclusions.

Passing binary data efficiently is impossible

It is impossible to pass binary data efficiently between JavaScript and Java through a @JavascriptInterface:

On the Java side:

@JavascriptInterface
void onBytes(byte[] bytes) {
   // bytes available here
}

And on the JavaScript side:

var byteArray = new Uint8Array(buffer);
var arr = new Uint8Array(byteArray.length);
for(var i = 0; i < byteArray.length; i++) {
  arr[i] = byteArray[i];
}
javaObject.onBytes(arr);

In the code above (from my old answer) and in Alex's - the conversion performed for the array is brutal:

case JavaType::TypeArray:
  if (value->IsType(base::Value::Type::DICTIONARY)) {
    result.l = CoerceJavaScriptDictionaryToArray(
        env, value, target_type, object_refs, error);
  } else if (value->IsType(base::Value::Type::LIST)) {
    result.l = CoerceJavaScriptListToArray(
        env, value, target_type, object_refs, error);
  } else {
    result.l = NULL;
  }
  break;

Which in turn coerces every array element to a Java object:

for (jsize i = 0; i < length; ++i) {
    const base::Value* value_element = null_value.get();
    list_value->Get(i, &value_element);
    jvalue element = CoerceJavaScriptValueToJavaValue(
        env, value_element, target_inner_type, false, object_refs, error);
    SetArrayElement(env, result, target_inner_type, i, element);

So, for a 1024 * 1024 * 10 Uint8Array - ten million Java objects are created and destroyed on each pass resulting in 10 seconds of CPU time on my emulator.

Creating an HTTP server

One thing we tried was creating an HTTP server and POSTing the result to it via an XMLHttpRequest. This worked - but ended up costing about 200ms of latency and also introduced a nasty memory leak.

MessageChannels are slow

Android API 23 added support for MessageChannels, which can be used via createWebMessageChannel() as shown in this answer. This is very slow, still serializes with GIN (like the @JavascriptInterface method) and incurs additional latency. I was not able to get this to work with reasonable performance.

It is worth mentioning that Google said they believe this is the way forward and hopes to promote message channels over @JavascriptInterface at some point.

Passing a string works

After reading the conversion code - one can see (and this was confirmed by Google) that the only way to avoid many conversions is to pass a String value. This only goes through:

case JavaType::TypeString: {
  std::string string_result;
  value->GetAsString(&string_result);
  result.l = ConvertUTF8ToJavaString(env, string_result).Release();
  break;
}

Which converts the result once to UTF8 and then again to a Java string. This still means the data (10MB in this case) is copied three times - but it is possible to pass 10MB of data in "only" 60ms - which is a lot more reasonable than the 10 seconds the above array method takes.

Petka came up with the idea of using 8859 encoding which can convert a single byte to a single letter. Unfortunately it is not supported in JavaScript's TextDecoder API - so Windows-1252 which is another 1 byte encoding can be used instead.

On the JavaScript side one can do:

var a = new Uint8Array(1024 * 1024 * 10); // your buffer
var b = a.buffer
// actually windows-1252 - but called iso-8859 in TextDecoder
var e = new TextDecoder("iso-8859-1"); 
var dec = e.decode(b);
proxy.onBytes(dec); // this is in the Java side.

Then, in the Java side:

@JavascriptInterface
public void onBytes(String dec) throws UnsupportedEncodingException
    byte[] bytes = dec.getBytes("windows-1252");
    // work with bytes here
}

Which runs in about 1/8th the time of direct serialization. It's still not very fast (since the string is padded to 16 bits instead of 8, then through UTF8 and then to UTF16 again). However, it runs in reasonable speed compared to the alternative.

After speaking with the relevant parties who are maintaining this code - they told me that it's as good as it can get with the current API. I was told I'm the first person to ask for this (fast JavaScript to Java serialization).

Answer from Benjamin Gruenbaum on Stack Overflow
🌐
Java
download.java.net › general › minion › javadoc › com › sun › labs › minion › util › buffer › ArrayBuffer.html
ArrayBuffer (Minion Search Engine)
java.lang.Object com.sun.labs.minion.util.buffer.StdBufferImpl com.sun.labs.minion.util.buffer.ArrayBuffer · All Implemented Interfaces: Buffer, ReadableBuffer, WriteableBuffer, java.lang.Cloneable · public class ArrayBuffer · extends StdBufferImpl · implements java.lang.Cloneable ·
Top answer
1 of 7
25

Ok, so following a chat with Google engineering and after reading the code I've reached the following conclusions.

Passing binary data efficiently is impossible

It is impossible to pass binary data efficiently between JavaScript and Java through a @JavascriptInterface:

On the Java side:

@JavascriptInterface
void onBytes(byte[] bytes) {
   // bytes available here
}

And on the JavaScript side:

var byteArray = new Uint8Array(buffer);
var arr = new Uint8Array(byteArray.length);
for(var i = 0; i < byteArray.length; i++) {
  arr[i] = byteArray[i];
}
javaObject.onBytes(arr);

In the code above (from my old answer) and in Alex's - the conversion performed for the array is brutal:

case JavaType::TypeArray:
  if (value->IsType(base::Value::Type::DICTIONARY)) {
    result.l = CoerceJavaScriptDictionaryToArray(
        env, value, target_type, object_refs, error);
  } else if (value->IsType(base::Value::Type::LIST)) {
    result.l = CoerceJavaScriptListToArray(
        env, value, target_type, object_refs, error);
  } else {
    result.l = NULL;
  }
  break;

Which in turn coerces every array element to a Java object:

for (jsize i = 0; i < length; ++i) {
    const base::Value* value_element = null_value.get();
    list_value->Get(i, &value_element);
    jvalue element = CoerceJavaScriptValueToJavaValue(
        env, value_element, target_inner_type, false, object_refs, error);
    SetArrayElement(env, result, target_inner_type, i, element);

So, for a 1024 * 1024 * 10 Uint8Array - ten million Java objects are created and destroyed on each pass resulting in 10 seconds of CPU time on my emulator.

Creating an HTTP server

One thing we tried was creating an HTTP server and POSTing the result to it via an XMLHttpRequest. This worked - but ended up costing about 200ms of latency and also introduced a nasty memory leak.

MessageChannels are slow

Android API 23 added support for MessageChannels, which can be used via createWebMessageChannel() as shown in this answer. This is very slow, still serializes with GIN (like the @JavascriptInterface method) and incurs additional latency. I was not able to get this to work with reasonable performance.

It is worth mentioning that Google said they believe this is the way forward and hopes to promote message channels over @JavascriptInterface at some point.

Passing a string works

After reading the conversion code - one can see (and this was confirmed by Google) that the only way to avoid many conversions is to pass a String value. This only goes through:

case JavaType::TypeString: {
  std::string string_result;
  value->GetAsString(&string_result);
  result.l = ConvertUTF8ToJavaString(env, string_result).Release();
  break;
}

Which converts the result once to UTF8 and then again to a Java string. This still means the data (10MB in this case) is copied three times - but it is possible to pass 10MB of data in "only" 60ms - which is a lot more reasonable than the 10 seconds the above array method takes.

Petka came up with the idea of using 8859 encoding which can convert a single byte to a single letter. Unfortunately it is not supported in JavaScript's TextDecoder API - so Windows-1252 which is another 1 byte encoding can be used instead.

On the JavaScript side one can do:

var a = new Uint8Array(1024 * 1024 * 10); // your buffer
var b = a.buffer
// actually windows-1252 - but called iso-8859 in TextDecoder
var e = new TextDecoder("iso-8859-1"); 
var dec = e.decode(b);
proxy.onBytes(dec); // this is in the Java side.

Then, in the Java side:

@JavascriptInterface
public void onBytes(String dec) throws UnsupportedEncodingException
    byte[] bytes = dec.getBytes("windows-1252");
    // work with bytes here
}

Which runs in about 1/8th the time of direct serialization. It's still not very fast (since the string is padded to 16 bits instead of 8, then through UTF8 and then to UTF16 again). However, it runs in reasonable speed compared to the alternative.

After speaking with the relevant parties who are maintaining this code - they told me that it's as good as it can get with the current API. I was told I'm the first person to ask for this (fast JavaScript to Java serialization).

2 of 7
4

It is pretty simple

Init section

 JavaScriptInterface jsInterface = new JavaScriptInterface(this);
 webView.getSettings().setJavaScriptEnabled(true);
 webView.addJavascriptInterface(jsInterface, "JSInterface");

JavaScriptInterface

public class JavaScriptInterface {
        private Activity activity;

        public JavaScriptInterface(Activity activiy) {
            this.activity = activiy;
        }
        @JavascriptInterface
        public void putData(byte[] bytes){
            //do whatever
        }
    }

Js section

<script>
  function putAnyBinaryArray(arr) {
        var uint8 = Uint8Array.from(arr);
        window.JSInterface.putData(uint8);
  };
</script>

TypedArray.from polyfill if need : https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/from

🌐
GeeksforGeeks
geeksforgeeks.org › java › buffer-array-methods-in-java-with-examples
Buffer array() methods in Java with Examples - GeeksforGeeks
June 28, 2019 - The array() method of java.nio.Buffer class is used to return the array that backs the taken buffer. This method is intended to allow array-backed buffers to be passed to native code more efficiently.
🌐
Java Tips
javatips.net › api › simpleframework-master › simple › simple-common › src › main › java › org › simpleframework › common › buffer › ArrayBuffer.java
ArrayBuffer.java example
*/ package org.simpleframework.common.buffer; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * The <code>ArrayBuffer</code> is intended to be a general purpose * byte buffer that stores bytes in an single internal byte array.
🌐
Simpleframework
simpleframework.org › doc › javadoc › org › simpleframework › util › buffer › ArrayBuffer.html
ArrayBuffer
The ArrayBuffer is intended to be a general purpose byte buffer that stores bytes in an single internal byte array. The intended use of this buffer is to provide a simple buffer object to read and write bytes with.
🌐
GitHub
gist.github.com › Exerosis › e75a4c263dc432dc56a57370fa9e6bec
ArrayBuffer.java · GitHub
ArrayBuffer.java · This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters ·
🌐
GeeksforGeeks
geeksforgeeks.org › java › bytebuffer-array-method-in-java-with-examples
ByteBuffer array() method in Java with Examples - GeeksforGeeks
June 3, 2021 - The array() method of java.nio.ByteBuffer class is used to return the byte array that backs the taken buffer. Modifications to this buffer's content will cause the returned array's content to be modified, and vice versa.
Top answer
1 of 2
11

Direct buffers are not meant to accelerate access from Java code. (If that were possible there was something wrong with the JVM’s own array implementation.)

These byte buffers are for interfacing with other components as you can write a byte buffer to a ByteChannel and you can use direct buffers in conjunction with native code such as with the OpenGL libraries you mentioned. It’s intended to accelerate these operation then. Using a graphics card’s chip for rendering can accelerate the overall operation to a degree more than compensating the possibly slower access to the buffer from Java code.

By the way, if you measure the access speed to a byte buffer, especially the direct byte buffers, it’s worth changing the byte order to the native byte order before acquiring a FloatBuffer view:

FloatBuffer bufferD = ByteBuffer.allocateDirect(SIZE * 4)
                                .order(ByteOrder.nativeOrder())
                                .asFloatBuffer();
2 of 2
5

Tldr:

Use direct buffers only if we need to do efficient high-speed I/O.

If we need efficient high-speed non-I/O operations, default array is the best choice.

If we need to do buffer-like operations on a default array, and we can afford to be slow, then use an array-backed buffer.

Tsdr:

Your tests did not test any I/O operations and it's conclusion is therefore wrong.

Your conclusion states (emphasis not mine):

Direct buffers should only be used if you worry about memory usage and never access the underlying data. They are slightly slower than non-direct buffers, much slower if the underlying data is accessed, but use less memory. In addition there is an extra overhead when converting non-byte data (like float-arrays) into bytes when using a direct buffer.

That is clearly wrong. Direct buffers are meant to solve speed problems, not memory ones problems. Direct buffers should be used whenever you need high-performance I/O access. This includes file/network operations etc. It is definitely faster when used correctly and is in fact the fastest that Java API provides out-of-the-box.

When doing file/network operations, there is an extra overhead when converting non-byte data into bytes. This is true for everything, not just direct buffers.

Your conclusion also states:

Note that a backened-buffer has a Java Array backening the content of the buffer. It is recommended to do operations on this back-buffer instead of looping put/get.

This is true, but you are missing the whole point of array-backed buffers. Array-backed buffers is a facade pattern on top of arrays. Array-backed buffers will never be faster than arrays themselves since internally they have to use the array.

As such, they are there for convenience, not for speed. In other words, if you need speed, it's recommended to choose array over array-facade. If you need convenience/readability, it's recommended to choose array-facade over array for buffer-like operations on array.

Also read:

  • How byte buffer works and why only direct buffers are useful (important!)

  • Java NIO's excerpt

🌐
GitHub
gist.github.com › ke4ktz › c78f5cb72c0d7ecaa190
Java: Output Byte Array Buffer to File · GitHub
Java: Output Byte Array Buffer to File. GitHub Gist: instantly share code, notes, and snippets.
Find elsewhere
🌐
Educative
educative.io › answers › what-is-the-bytebuffer-array-method-in-java
What is the ByteBuffer array() method in Java?
In Java, the array() method of the ByteBuffer class returns the array that backs a provided ByteBuffer object.
Top answer
1 of 2
3

It turns out that as long as you don't look too closely at it, a JS Int8Array (and so a elemental2.core.Int8Array) behaves like you'd expect a GWT/Java byte[] to do - it will only contain values from -127 to 128. Unlike a "real" byte[], it will not correctly cast to byte[] and anything calling .getClass() or other Java methods on it will not work correctly either. It also can't be cast (by generics or explicitly) to byte[] either.

So if you need to treat the contents as a real Java array, you have to copy it first. Your answer will do what you want, but it might be more clearly correct to not "convert it from Double" along the way:

import elemental2.core.ArrayBuffer;
import elemental2.core.Int8Array;
import jsinterop.base.Js;

public static byte[] toBytes(ArrayBuffer buffer) {
    // Use uncheckedCast since we are outright lying to the compiler
    byte[] arr = Js.uncheckedCast(new Int8Array(buffer));
    byte[] result = new byte[arr.length];
    for (int i = 0; i < arr.length; i++) {
        result[i] = arr[i];
    }
    return result;
}

Your solution is also correct - a java.lang.Double in GWT and J2CL is exactly the same as a JS Number type, which is what your Int8Array is going to return when you query its values. The byteValue() call you're making will ask the compiler to make certain that the values are what is expected - it will truncate (probably via value & 0xFF?) the value, which should be cheap, but not entirely free.

Finally as long as you are just going to treat the resulting byte[] as a collection you can iterate, and won't be dealing with it in some way that the actual type will be checked, we can get away with not copying it at all:

import elemental2.core.ArrayBuffer;
import elemental2.core.Int8Array;
import jsinterop.base.Js;

public static byte[] toBytes(ArrayBuffer buffer) {
    byte[] result = Js.uncheckedCast(new Int8Array(buffer));
    return result;
}

This really is cheating though, and has other downsides, like if the original buffer is modified the array will be changed too, and vice versa. But it is very fast!

2 of 2
1

Here's what I've got so far:

import elemental2.core.ArrayBuffer;
import elemental2.core.Int8Array;

public static byte[] toBytes(ArrayBuffer buffer) {
    Int8Array arr = new Int8Array(buffer);
    byte[] result = new byte[arr.length];
    for (int i = 0; i < arr.length; i++) {
        Double elem = arr.getAt(i);
        result[i] = elem.byteValue();
    }
    return result;
}
Author: TooTallNate
🌐
Baeldung
baeldung.com › home › scala collections › guide to arraybuffer
Guide to ArrayBuffer | Baeldung on Scala
March 18, 2024 - There are two main groups of collections in Scala: immutable and mutable collections. While the Scala community is biased towards the usage of immutable data structures, the standard lib also provides many mutable collections. One such example is the ArrayBuffer class which is very similar to normal Java Lists.
🌐
JavaScript.info
javascript.info › tutorial › binary data, files
ArrayBuffer, binary arrays
July 11, 2022 - So, the binary data in an ArrayBuffer of 16 bytes can be interpreted as 16 “tiny numbers”, or 8 bigger numbers (2 bytes each), or 4 even bigger (4 bytes each), or 2 floating-point values with high precision (8 bytes each).
🌐
Scala Documentation
docs.scala-lang.org › overviews › scala-book › arraybuffer-examples.html
The ArrayBuffer Class | Scala Book | Scala Documentation
It’s a mutable sequence, so you can use its methods to modify its contents, and those methods are similar to methods on Java sequences. ... scala> ints += 1 res0: ints.type = ArrayBuffer(1) scala> ints += 2 res1: ints.type = ArrayBuffer(1, 2)
🌐
Stack Overflow
stackoverflow.com › questions › 61942349 › java-object-serialize-with-nested-arraybuffer
Java Object Serialize With Nested ArrayBuffer?
JSON has no concept of an ArrayBuffer so you can't serialize to one. You will have to determine what Jackson is outputting(It looks like a Base64 String) for a ByteBuffer and write your own deserializer in Javascript.