Graaljs: Custom Java -> JS Conversion

Created on 11 Apr 2019  路  11Comments  路  Source: oracle/graaljs

The new HostAccess.Builder#targetTypeMapping API (see https://github.com/graalvm/graaljs/issues/94) is fantastic for JS -> Java conversion. Thank you!

Can we similarly handle Java -> JS conversion? I tried using .targetTypeMapping(MyPojo.class, Value.class, ..., ...), but it was not triggered when:

  • Using context.getBindings("js").putMember("myPojo", myPojoInstance), but this probably makes sense, because it's added as a host object
  • Calling value.execute(myPojoInstance) where value is a JS function accepting an arg.

Does targetTypeMapping only apply to JS -> Java conversions? If it does apply to Java -> JS conversions, how can I correctly use it?

enhancement

Most helpful comment

We have plans to add first-class date support in interop. So we will do that conversion for you. (actually we won't convert, we will just interpret Java dates as JS dates and vice versa).
You have another use-case?

All 11 comments

Bump... Any thoughts @chumer ? Is targetTypeMapping only for JS -> Java? Is there any equivalent Java -> JS mechanism? Thanks.

No there is currently no equivalent mapping mechanism from Host Java to JS.
Did you try to use a base class for the pojo classes that implement ProxyObject? That should allow you to customize how the guest application sees the object. The best thing about that approach is that it does not require any conversion.

Thanks for the response. Yes we've been using ProxyObject... a lot. But we might be able to eliminate a bunch of proxies with equivalent Java -> JS type mapping.

One example involves an object with a date, where we want it to be both a Java Date and a JS Date. It needs conversion even when the parent object implements ProxyObject. I suppose one might be able to make some complex 'Date' proxy that fulfills both Java and JS without conversion... but that doesn't sound too palatable...

We have plans to add first-class date support in interop. So we will do that conversion for you. (actually we won't convert, we will just interpret Java dates as JS dates and vice versa).
You have another use-case?

@chumer do you have any ETA when Date support will be released?

Currently I have an issue with passing js Date object to java, because graal mapped them to long value.
Looks like it cannot be handled though targetMapping

@mdsina For JS-Date to Java-Date, HostAccess.Builder#targetTypeMapping does work for us. We do need to use a proxy, though, so a Date created in Java will act (getTime function) as a JS-Date in Javascript. Also note if you're later converting to something like JSON, it would need to be either a long or a string or nested object since JSON has no date-specific type.

@chumer Another simpler case we've encountered is:

  • Java POJO with a field that is an enum
  • In JS we'd like the resulting object (from the POJO) to be treated as a string primitive (the Enum#name()). We also are actually working with TypeScript so the definition can be a union of strings (corresponding to the enum values)
  • For js->java, we can use targetTypeMapping to get the enum from the js string
  • For java->js, we're forced to make the POJO a proxy (or wrap it in one) to call name() on the enum. FWIW I tried making an EnumProxy that would simply return a String, but despite https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/proxy/Proxy.html suggesting one could proxy primitives, I couldn't find a way. ProxyNativeObject#asPointer() was not called and is probably for C/native things only. ProxyPrimitive was removed? Doesn't seem like any Proxy will work for this use case

If we had the equivalent java->js type mapping, we wouldn't need every POJO containing an enum to be/use a proxy.

And while the enum and Date are simple example cases, it's the same story anywhere a proxy/conversion is needed... Either the class is replaced with a proxy (everywhere used), or every parent container must become a proxy to handle the field (at least for Java enum <-> JS String, see above).

If we had Java->Js type mapping, such a proxy could be defined once there, and automatically handled by the the system, including nested situations (field of a parent POJO) etc.

Your thoughts, @chumer ?
Many thanks.

Hi @chumer , I'm pinging this again since:

  • We're still forced to use a ProxyObject for dates, see https://github.com/graalvm/graaljs/issues/200
  • Wanted to get your thoughts on the enum use-case above. A string/primitive proxy could help there. Although Java->JS type mapping would still be awesome :)

I wanted to chime in with another use-case for this that first-class dates don't cover.

I wanted to pass Jackson's JsonNode values into JS, and tried to define a ProxyObject and ProxyArray around the ObjectNode and ArrayNode respectively. This caused a problem, though.

In Jackson, accessing an element of a JsonNode returns another JsonNode, so my ProxyObject object needs to wrap the result in another proxy before returning it from ProxyObject::get. However, if I wrap a _new_ Proxy instance around it, I have the following issue:

Suppose I have an object with a nested object, represented as a JsonNode.

x = { foo: {bar: 'old' } }

I create a proxy around that value and pass it into the following JS function:

function(x) {
    foo1 = x.foo;
    foo2 = x.foo;
    foo1.bar = 'new';
    return [ Object.is(foo1, foo2), foo2.bar];
}

This returns[ false, 'new' ]. ProxyObject::get returned two different proxy objects around the same underlying JsonNode object. JS interprets these as separate objects because it treats the Proxy object as the object to compare by identity, not the underlying object. In order to solve this, I need to store, along with my Graal Context, a mapping of JsonNodes to ProxyObjects to make sure that I always return the same ProxyObject for the same underlying object. This seems tedious, and I understand (maybe incorrectly) that Graal already does this. (Hence why if I pass the same Proxy object into Context::asValue, JS _does_ understand that they represent the same object.)

If Graal provided a way to define a custom mapping for Java objects when passing them into Graal, I could define that function to wrap a JsonNode in a ProxyObject. Graal would then, I presume, track the JsonNode's identity correctly in the already-existing code-path, making this use-case _much_ simpler.

Was this page helpful?
0 / 5 - 0 ratings