Open
Issue #124 · created by Ian Preston ·


creating a Java byte[] in JS

I'm having trouble figuring out how to create a Java byte[] in Javascript to pass to a Java function. An example for this in the docs would be helpful.

It also seems that if I write a Java class with a static method to create and return a byte[], then the returned object in Js has the correct length, but every element is undefined, rather than a U8 or I8. Internally in doppio, in a native JS function, there is a helper function to create arrays (Doppio.VM.Util.newArrayFromDataWithClass), but that doesn't work outside the JVM.


3 participants
  • No avatar
    Jim Sproch @jimsproch

    cc @hrjet9

    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512 Harshad RJ @hrjet9

    Reassigned to @hrjet9

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9 ·

    @ianopolous

    For (1), the way to pass a byte[] into Java is to simply pass a JS array of numbers to Java. The fuzzy matcher should take care of finding a good match for the Java function. However, that is currently not handling arrays correctly.

    (2) seems to be a hole in our implementation.

    I am looking into these, but it's past midnight here; ETA tomorrow.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Ian Preston @ianopolous

    Thanks again for the speedy reply. So passing in a Uint8Array should work, or does it need to be a Int8Array?

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    @hrjet9 I'm not sure the fuzzy matcher should convert arrays. It's a tricky situation. I'm curious to hear your thoughts.

    The issue is that javascript allows additional data to be tacked onto an array.

    var foo = [1,2,3];
     > undefined
    Array.isArray(foo)
     > true
    foo.bar = 'noise';
     > "noise"
    Array.isArray(foo);
     > true
    foo.bar
     > "noise"
    

    If this gets converted into an array of three integers, there are a couple of problems:

    1. It is impossible to read the bar property off the object. But Java should have a way of reflecting all the properties of the javascript object.
    2. If Java clones the array and returns a new array with the exact same properties, from a Java perspective, it is an exact copy, but when you pass that new array into Javascript land, it will be missing the bar property.

    My intuition is that a javascript array should be reflected into Java land as a JSObject. If you want a reference to a Java array in Javascript land, you would need to call a simple Java function that returns a new Java array.

    What do you guys think?

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Ian Preston @ianopolous

    Do people do that? (rely on an array object having other random properties)? That seems like an anti-pattern to me.

    We will definitely need a way to pass pure JS byte arrays into functions that expect pure Java byte arrays. The arrays involved will be large (sometimes more than 500MiB) so avoiding unnecessary copying would be great. I'd be fine with a util method, like in Doppio, for converting a Uint8Array to a byte[], so the matcher doesn't need to convert arrays.

    Given that Java doesn't really have unsigned integers, I think converting JS unsigned integers (or arrays of) to the signed Java equivalent of the same bit width (or an array of) is intuitive and should be cheap. But maybe the util converter methods would be best.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch ·

    Unfortunately, yes, people rely on it. They're called "Expando properties", and they're pretty common for a whole variety of reasons. One common use case is that Javascript (until recently) didn't support a Map<Object, Object>. The only thing you could do was Map<String, Object> (by virtue of string properties on an object). But one way you could work around this was by using a special/unique expando prop on an object that points to another object, which effectively allowed you to simulate a Map<Object, Object>, but which required adding a random/unexpected field to every key object. It takes a minute to wrap your head around, but it works. Best practice or not, it is the only way to support Object->Object maps in a non-ES6 browser. An example of usage is here: https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactInstanceMap.js

    Anyway, I like your idea of a converter method. The object could enter Java land as a JSObject, and we can have a function that takes a JSObject and "unwraps" it into some sort of native representation. We just need to make sure the semantics are well-defined with both a system/webworker vm as well as doppio running on the main thread.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    I'm not sure the fuzzy matcher should convert arrays. It's a tricky situation. I'm curious to hear your thoughts. The issue is that javascript allows additional data to be tacked onto an array.

    Currently, if the caller (in JS land) wraps the array in an object before sending to Java, it will be reflected as a JSObject into Java. Java land can then extract the original JSValue from the incoming JSObject.

    Hence, keeping the existing behavior allows some convenience:

    • Automatic conversion of arrays to Java equivalents for the simple use-cases and
    • Manual wrapping of arrays into objects, to preserve their object nature, for the more advanced use-cases.

    Anyway, I like your idea of a converter method.

    Agreed; we can track it as a separate issue.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    Suppose I have a Java function:

    public int[] doNothing(byte[] input) {
      return input;
    }
    

    And then a javascript caller:

    var original = [1,2,3];
    original.bar = "noise";
    var returned = await doNothing(original);
    console.log(original === returned);  // Does this print true or false?  Intuitively, it should print true.
    console.log(returned.bar);  // Does this print "noise" or undefined?  It feels super wrong for it to loose the expando
    

    Intuitively, it feels wrong for the returned object to not preserve identity, and it feels super wrong for it to loose the expando property. That's just my two cents.

    I think my ambivalence comes from the fact that I want arrays to "just work", but I'm not sure that's the right call from a correctness perspective. At the very least, I intuitively expect "same object in, same object out" to work. Thoughts?

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9 ·

    console.log(original === returned);

    I don't think this will be possible to implement on SystemJVM, since there is no way to add meta-data to the JVM array that indicates where the original array came from.

    On Doppio JVM, it might be possible to do this by adding some meta data to the JVM array. But it will also change the ownership semantics of our API: An array passed to Java will not have to be accessed / modified until it has been completely un-referenced from the JVM.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    since there is no way to add meta-data to the JVM array that indicates where the original array came from.

    Thinking further, we could do it instead by associating the meta-data with a weak-reference, and then comparing references.

    So, the only remaining hurdle is the ownership semantics.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    I don't think this will be possible to implement on SystemJVM, since there is no way to add meta-data to the JVM array that indicates where the original array came from.

    On Doppio JVM, it might be possible to do this by adding some meta data to the JVM array. But it will also change the ownership semantics of our API: An array passed to Java will not have to be accessed / modified until it has been completely un-referenced from the JVM.

    This is why I was suggesting that the javascript array be a JSObject. Then all the expected semantics (like reference equality) are naturally preserved. You can access/edit the expando properties, etc. The JSObject is still owned by javascript, so mutations cross the bridge and the ownership semantics are preserved, etc.

    If you want to get an integer array or whatever, you can call a helper function which returns you an integer array but doesn't preserve identity semantics.

    int[] myArray = JSObjectUnsafeHelpers.asIntArray(myjsobject);

    This is not quite as convenient as automatically fuzzy matching, but it means all the semantics are preserved by default, and people can call a native helper if they want different semantics.

    Otherwise, we're going to be screwed by edge cases. For instance, what happens if the javascript array gets passed to a javascript function that pushes an additional element into the array. In Java, this necessarily requires creating a new array (because of Java's array semantics) but in Javascript it is just a mutation of the original array. There are so many edge cases, it seems like an intractable problem to me.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    Similarly, when a Java array goes into JS land, it is really a wrapper object that has a setter&getter.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    This is why I was suggesting that the javascript array be a JSObject.

    Yeah, JSObject is a good solution, but then the Java function signature would become:

    public JSObject doNothing(JSObject input) {
      return input;
    }
    

    In which case, the JS side can also explicitly pass an object: doNothing({myArray: [1,2,3]}).

    This way, we don't lose the convenience of passing JS arrays to Java functions that accept arrays. And the wrapping in JS is explicit (which I think is a benefit).

    Further, if we implicitly convert JS arrays to JSObject, then JS won't be able to call Java functions that accept arrays directly, unless we provide some other mechanism for it.

    There are so many edge cases, it seems like an intractable problem to me.

    I don't see a big problem actually. I guess that's because I never saw arrays crossing the JS-JVM boundary as pass-by-reference, but rather pass-by-value. Only objects were pass-by-reference. There is some inconsitency there, yes, but it still feels intuitive to me. Maybe I am biased as a developer.

    Let me know what you finally decide. I have the fuzzy matcher working with arrays in my local branch.

    If we have have to treat arrays as JSObjects, then I could start that implementation.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    Further, if we implicitly convert JS arrays to JSObject, then JS won't be able to call Java functions that accept arrays directly, unless we provide some other mechanism for it.

    Actually, JS can. But the array must have been created in Java land. It must be a Java array instead of a Javascript array.

    I never saw arrays crossing the JS-JVM boundary as pass-by-reference, but rather pass-by-value. Only objects were pass-by-reference.

    I think the key is that in Javascript... array extends object, so all arrays are objects. In both Java and Javascript, arrays are passed by reference, so that seems to align better with both languages.

    In fact, Javascript arrays are more like Java Lists. If we fuzzy match javascript arrays, it almost seems like we should fuzzy-match them to lists, since lists are semantically closer (easily change size, default data-structure used everywhere, pass by reference, etc).

    I have the fuzzy matcher working with arrays in my local branch.

    How do you handle the following cases?

    • A nested array: [[1,2,3],[4,5,6],[7,8,9]]
    • What happens when a javascript function calls a Java function with a javascript object that happens to be an array? Will the fuzzy matcher ever convert it to a JSObject?
    • Mixed arrays: JavaClass.takesIntArray([1,2,3,4,5,6,7,"foo-bar-noise-is-not-a-number"]). Wouldn't you need to iterate over the entire array in order to verify the type? For a large array like @ianopolous was talking about (500mb), that seems like a prohibitively slow operation.
    • What do you do with typed arrays? What happens if I pass a UInt16Array to a Java function that takes in an int[] array?

    I actually think the key factor, however, is that we loose both identity and expandos when we cross the boundary. Both of these can be very subtle bugs. It won't be immediately obvious what went wrong, nor will it be obvious how to fix the problem (thinking to wrap it in a jsobject requires a relatively detailed knowledge of the fuzzy matcher's semantics).

    I know the fuzzy matcher is super convinent, and that's what makes this so tempting (especially since arrays are used literally everywhere in javascript)... but the more I think about it, the more I feel like this is a rabbit hole that we could easily close by just saying "it's just a JSObject".

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Ian Preston @ianopolous

    The main requirement from our side is that we can pass a JS Int8Array or ideally a Uint8Array (possibly via a JS helper function) directly into a Java function that takes a byte array, without copying the JS array.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    @ianopolous

    without copying the JS array.

    This is the hard part. We might be able to avoid making a copy in Doppio with a Doppio specific hack, but it's not possible in System JVM. And having two different semantics would be confusing.

    Using JSObject will avoid the copy but only if your Java function iterates the array using something like myJSObject.getByte(index). If you try to get the array contents as byte[], it will become a copy again.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    @jimsproch

    I agree, treating a JS array as JSObject is cleaner. Though, just to ensure we are in sync, JS won't be able to call any Java functions that accept arrays of any type.

    To answer your questions:

    How do you handle the following cases? A nested array: [[1,2,3],[4,5,6],[7,8,9]]

    It will become this: (Array<Object>)(new Array<Array<Double>>()) with the values copied into the nested array. The matcher will pick a Java function that accepts Array<Object>.

    What happens when a javascript function calls a Java function with a javascript object that happens to be an array? Will the fuzzy matcher ever convert it to a JSObject?

    The wrapping code uses this check Array.isArray(obj) when deciding to use the array handling logic. If that check fails, it becomes a JSObject.

    Wouldn't you need to iterate over the entire array in order to verify the type?

    Yes :(

    For a large array like @ianopolous was talking about (500mb), that seems like a prohibitively slow operation.

    Yeah, the convenience comes at a price.

    What do you do with typed arrays?

    The Array.isArray() check will fail, so it becomes a JSObject.

    What happens if I pass a UInt16Array to a Java function that takes in an int[] array?

    It will fail to find a match.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch ·

    I agree, treating a JS array as JSObject is cleaner. Though, just to ensure we are in sync, JS won't be able to call any Java functions that accept arrays of any type.

    I don't understand. The following code is broken (due to a JavaPoly or Doppio bug, I presume), but it should work, right? So it's not super convenient, but users could still invoke a function that takes in an array.

    <script src="build/javapoly.js"></script>
    <script src="http://www.jimsproch.com/react/babel-browser.js"></script>
    <script type="text/java">
            public class MyClass {
                public static void myFunction(int[] array) {
                    System.out.println("The length of the array is: "+array.length);
                }
            }
    </script>
    <script type="text/babel">
    window.main = async function(){
        var Class = await JavaPoly.type("java.lang.Class");
        var intClass = await (await (await Class.forName("java.lang.Integer")).getField("TYPE")).get(null);
        var array = await (await JavaPoly.type("java.lang.reflect.Array")).newInstance(intClass, 4);
    
        await (await JavaPoly.type("MyClass")).myFunction(array);
    };
    </script>
    <button onClick="window.main()">Open the browser console and then click me</button>
    

    Using JSObject will avoid the copy but only if your Java function iterates the array using something like myJSObject.getByte(index). If you try to get the array contents as byte[], it will become a copy again.

    Yeah, but we could provide an unsafe helper that works in doppio and provides the array without doing a copy: byte[] myArray = JSObjectUnsafeHelpers.asByteArray(myjsobject);

    It will become this: (Array)(new Array>()) with the values copied into the nested array. The matcher will pick a Java function that accepts Array.

    It will become this: (Array)(new Array>()) with the values copied into the nested array. The matcher will pick a Java function that accepts Array.

    So does that mean if the user defines public static myFunction(int[][] arr){};, we would not match that function?

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Ian Preston @ianopolous

    This is an example of JS code inside a native doppio method that receives, calls and returns Java byte[]'s without any copying (to the best of my knowledge):

    'crypto_sign([B[B)[B': function(thread, javaThis, message, secretKey) {
            thread.setStatus(ThreadStatus.ASYNC_WAITING);
            var res = nacl.sign(new Uint8Array(message.array), new Uint8Array(secretKey.array));
            var i8Array = new Int8Array(res.buffer, res.byteOffset, res.byteLength);
            var javaByteArray = Doppio.VM.Util.newArrayFromDataWithClass(thread, thread.getBsCl().getInitializedClass(thread, '[B'), i8Array);
            thread.asyncReturn(javaByteArray);
        }
    
    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    Ah yes, capturing a Java array and passing it back to Java will work. Note that we don't support capture of Java objects on web-workers and SystemJVM.

    we could provide an unsafe helper that works in doppio and provides the array without doing a copy:

    Yeah, doppio specific support is possible. Will not work for workers (hah).

    So does that mean if the user defines public static myFunction(int[][] arr){};, we would not match that function?

    Hmm.. looks like primitive arrays are not type erased in Java. In that case, my bad, it should match. The implementation of JS array to Java array conversions should be recursive and be able to handle nested non-type-erased arrays (in case we continue to go with current behavior).

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Ian Preston @ianopolous

    Ah yes, capturing a Java array and passing it back to Java will work. Note that we don't support capture of Java objects on web-workers and SystemJVM.

    The example is not passing a byte[] from Java back to java, it's creating a new byte[] from "res" Uint8Array created by the JS nacl library.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    @ianopolous

    The example is not passing a byte[] from Java back to java, it's creating a new byte[] from "res" Uint8Array created by the JS nacl library.

    Oh, sorry, I was replying to @jimsproch. I didn't see your reply while I was typing. We should probably switch to a threaded forum / mailing list.

    About your example: nice to know that Doppio accepts the Uin8Array. thanks!

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    Replying to self:

    be able to handle nested non-type-erased arrays

    I just re-learnt that all arrays in Java are never type-erased (not just primitive arrays). I have been spoilt by languages like Scala and Kotlin, which allow you think of Arrays as classes.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • No avatar
    Jim Sproch @jimsproch

    Note that we don't support capture of Java objects on web-workers and SystemJVM.

    Shit, I almost forgot about that :P. Yes, I suppose that does mean that those functions can't be called from pure javascript. I think I'm ok with that, at least for now. Someone can always expose a helper function that takes in a JSObject (javascript array), converts it to a Java array (either using a copy for safety, or unsafe for performance on doppio), and passes it to a Java function that takes in an array.

    Honestly, I think I don't really care what we do here. My intuition is to not fuzzy-match javascript arrays, although I can totally see how fuzzymatching them would be very convenient. If we're going to do fuzzy matching, I'd be tempted to have it match the java.util.List interface, since Javascript array feels more analogous to a java.util.List. Realistically, the only similarity between a Javascript array and a Java array is the name :P.

    Perhaps... public class JavascriptArray extends JSObject implements List<Object> {...}

    That would allow us to keep identity, expandos, and better matches the Javascript array functionality. If someone wants a real array, they can call List.toArray() in a helper function.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512 Harshad RJ @hrjet9

    mentioned in merge request !30

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
  • 1419208586 beethoven 512
    Harshad RJ @hrjet9

    @jimsproch I have created a merge request !30 for the code I had already written until the point: "I have the fuzzy matcher working with arrays in my local branch."

    If you are sure that we should go with the most recent solution that you proposed (public class JavascriptArray extends JSObject implements List<Object> {...}), I can start working towards it. I am not sure what it would mean for webworkers and SystemJVMs and whether I should bother about them or not.

    Edit in fullscreen
    Comments are parsed with GitLab Flavored Markdown
    Attach images (JPG, PNG, GIF) by dragging & dropping or selecting them.
jimsproch/JavaPoly#124

Assignee: Harshad RJ

Milestone: none


Votes
0 up
0 down