GraalVM and proxy bindings with embedded languages.

Mix and match as you go…
November 26, 2021 by Michael

A while ago I had the opportunity to publish a post in the GraalVMs Medium blog titled The many ways of polyglot programming with GraalVM which is still accurate.

A year later, the GraalVM just got better in many dimensions: Faster, supporting JDK17 and I think its documentation is now quite stellar. Have a look at both the polyglot programming and the embeddings language. The later is what we are referring to in this post.

The documentation has excellent examples about how to access host objects (in my example Java objects) from the embedded language and vice versa. First, here’s how to access a host object (an instance of MyClass) from embedded JavaScript:

public static class MyClass {
    public int               id    = 42;
    public String            text  = "42";
    public int[]             arr   = new int[]{1, 42, 3};
    public Callable<Integer> ret42 = () -> 42;
}
 
public static void main(String[] args) {
    try (Context context = Context.newBuilder()
                               .allowAllAccess(true)
                           .build()) {
        context.getBindings("js").putMember("javaObj", new MyClass());
        boolean valid = context.eval("js",
               "    javaObj.id         == 42"          +
               " && javaObj.text       == '42'"        +
               " && javaObj.arr[1]     == 42"          +
               " && javaObj.ret42()    == 42")
           .asBoolean();
        assert valid == true;
    }
}

This is the essential part: context.getBindings("js").putMember("javaObj", new MyClass());. The instance is added to the bindings of JavaScript variables in the polyglot context. In the following eval block, a boolean expression is defined and returned, checking if all the values are as expected.

Vice versa, accessing JavaScript members of the embedded language from the Java host looks like this:

try (Context context = Context.create()) {
    Value result = context.eval("js", 
                    "({ "                   +
                        "id   : 42, "       +
                        "text : '42', "     +
                        "arr  : [1,42,3] "  +
                    "})");
    assert result.hasMembers();
 
    int id = result.getMember("id").asInt();
    assert id == 42;
 
    String text = result.getMember("text").asString();
    assert text.equals("42");
 
    Value array = result.getMember("arr");
    assert array.hasArrayElements();
    assert array.getArraySize() == 3;
    assert array.getArrayElement(1).asInt() == 42;
}

This time, a result is defined directly in the JavaScript context. The result is a JavaScript object like structure and its values are asserted. So far, so (actually) exciting.

There is a great api that allows what in terms of members and methods can be accessed from the embedding (read more here) and we find a plethora of options more (how to scope parameters, how to allow access to iterables and more).

The documentation is however a bit sparse on how to use org.graalvm.polyglot.proxy.Proxy. We do find however a good clue inside the JavaDoc of the aforementioned class:

Proxy interfaces allow to mimic guest language objects, arrays, executables, primitives and native objects in Graal languages. Every Graal language will treat instances of proxies like an object of that particular language.

So that interface essentially allows you to stuff a host object into the guest and there it behaves like the native thing. GraalVM actually comes with a couple of specializations for it:

  • ProxyArray to mimic arrays
  • ProxyObject to mimic objects with members
  • ProxyExecutable to mimic objects that can be executed
  • ProxyNativeObject to mimic native objects
  • ProxyDate to mimic date objects
  • ProxyTime to mimic time objects
  • ProxyTimeZone to mimic timezone objects
  • ProxyDuration to mimic duration objects
  • ProxyInstant to mimic timestamp objects
  • ProxyIterable to mimic iterable objects
  • ProxyIterator to mimic iterator objects
  • ProxyHashMap to mimic map objects

Many of them provide static factory methods to get you an instance of a proxy that can be passed to the polyglot instance as in the first example above. The documentation itself has an example about array proxies. The question that reached my desk was about date related proxies, in this case a ProxyInstant, something that mimics things representing timestamps in the guest. To not confuse Java programmers more than necessary, JavaScript has the same mess with it’s Date object than what we Java programmers have with java.util.Date: A think to represent it all. Modern Java is much more clearer these days and call it what it is: An java.time.Instant (An instantaneous point on the time-line).

So what does ProxyInstant do? ProxyInstant.from(Instant.now()) gives you an object that when passed to embedded JavaScript behaves in many situation like JavaScripts date. For example: It will compare correctly, but that’s pretty much exactly how far it goes.

Methods like getTime, setTime on the proxy inside the guest (at least in JavaScript) won’t work. Why is that? The proxy does not map all those methods to the JavaScripts object members and it actually has no clue how: The proxy can be defined on a Java instant, date or nothing thereof at all and just use a long internally…

So how to solve that? Proxies in the host can be combined and we add ProxyObject:

public static class DateProxy implements ProxyObject, ProxyInstant {
}

ProxyObject comes with getMember, putMember, hasMember and getMemberKeys. In JavaScript, both attributes and methods of an object are referred to as members so that is exactly what we are looking for to make for example getTime working. One possible Proxy object to make Java’s instant or date work as JavaScript date inside embedded JS on GraalVM therefor looks like this

@SuppressWarnings("deprecation")
public static class DateProxy implements ProxyObject, ProxyInstant {
 
  private static final Set<String> PROTOTYPE_FUNCTIONS = Set.of(
    "getTime",
    "getDate",
    "getHours",
    "getMinutes",
    "getSeconds",
    "setDate",
    "setHours",
    "toString"
  );
 
  private final Date delegate;
 
  public DateProxy(Date delegate) {
    this.delegate = delegate;
  }
 
  public DateProxy(Instant delegate) {
    this(Date.from(delegate));
  }
 
  @Override
  public Object getMember(String key) {
    return switch (key) {
      case "getTime" -> (ProxyExecutable) arguments -> delegate.getTime();
      case "getDate" -> (ProxyExecutable) arguments -> delegate.getDate();
      case "setHours" -> (ProxyExecutable) arguments -> {
        delegate.setHours(arguments[0].asInt());
        return delegate.getTime();
      };
      case "setDate" -> (ProxyExecutable) arguments -> {
        delegate.setDate(arguments[0].asInt());
        return delegate.getTime();
      };
      case "toString" -> (ProxyExecutable) arguments -> delegate.toString();
      default -> throw new UnsupportedOperationException("This date does not support: " + key);
    };
  }
 
  @Override
  public Object getMemberKeys() {
    return PROTOTYPE_FUNCTIONS.toArray();
  }
 
  @Override
  public boolean hasMember(String key) {
    return PROTOTYPE_FUNCTIONS.contains(key);
  }
 
  @Override
  public void putMember(String key, Value value) {
    throw new UnsupportedOperationException("This date does not support adding new properties/functions.");
  }
 
  @Override
  public Instant asInstant() {
    return delegate.toInstant();
  }
}

Most of the logic is in hasMember and the actual dispatch in getMember: Everything that a member can represent can be returned! So either concrete values that are representable inside the embedded language or proxy objects again. As we want to represent methods on that JavaScript object we return ProxyExecutable! Execution will actually be deferred until called in the guest. What happens in the call is of course up to you. I have added examples for just getting values from the delegate but also for manipulating it. Because of the later I found it sensible to use a java.util.Date as delegate, but an immutable Instant on a mutable attribute of the proxy object would have been possible as well.

Of course there are methods left out, but I think the idea is clear. The proxy object works as expected:

public class Application {
 
  public static void main(String... a) {
 
    try (var context = Context.newBuilder("js").build()) {
 
      var today = LocalDate.now();
      var bindings = context.getBindings("js");
      bindings.putMember("javaInstant", new DateProxy(today.atStartOfDay().atZone(ZoneId.of("Europe/Berlin")).toInstant()));
      bindings.putMember("yesterday", new DateProxy(today.minusDays(1).atStartOfDay().atZone(ZoneId.of("Europe/Berlin")).toInstant()));
      var result = context.eval("js",
        """
          var nativeDate = new Date(new Date().toLocaleString("en-US", {timeZone: "Europe/Berlin"}));
          nativeDate.setHours(12);
          nativeDate.setMinutes(0);
          nativeDate.setSeconds(0);
          nativeDate.setMilliseconds(0);
 
          javaInstant.setHours(12);
          ({
            nativeDate   : nativeDate,
            nativeTimeFromNativeDate : nativeDate.getTime(),
            javaInstant: javaInstant,
            diff: nativeDate.getTime() - javaInstant.getTime(),
            isBefore: yesterday < nativeDate,
            nextWeek: new Date(javaInstant.setDate(javaInstant.getDate() + 7))
          })
          """);
 
 
      assertThat(result.getMember("nativeDate").asDate()).isEqualTo(today);
      assertThat(result.getMember("diff").asLong()).isZero();
      assertThat(result.getMember("isBefore").asBoolean()).isTrue();
      assertThat(result.getMember("nextWeek").asDate()).isEqualTo(today.plusWeeks(1));
    }
  }
}

As always, there are two or more sides to solutions: With the one above, you are in full control of what is possible or not. On the other hand, you are in full control of what is possible or not. There will probably edge cases if you pass in such a proxy to an embedded program which in turn calls things on it you didn’t foresee. On the other hand, it is rather straight forward and most likely performant without too many context switches.

The other option would be pulling a JavaScript date from the embedded into the Java host like so var javaScriptDate = context.eval("js", "new Date()"); and manipulate it there.

Either way, I found it quite interesting to dig into GraalVM polyglot again thanks to one of our partners asking great questions and I hope you find that insight here useful as well. A full version of that program is available as a runnable JBang script:

jbang https://gist.github.com/michael-simons/556dd49744aae99a72aa466bd3b832a0

As you might have noticed in the snippets above, I am on Java 17. The script runs best GraalVM 21.3.0 JDK 17 but will also be happy (more or less) on stock JDK 17.

4 comments

  1. Gavin Ray wrote:

    Thank you for taking the time to do this overview, I wasn’t certain how Proxy objects worked exactly.

    The only part I’m not sure I understand is where/how exactly you declared that your class was meant to override the native JS “Date” object?

    You have “class DateProxy implements ProxyObject, ProxyInstant”, is it due to implementing ProxyInstant that it becomes usable as a JS “Date” object?

    What about “ProxyDate”, is that the same effect?

    Posted on December 24, 2021 at 7:01 PM | Permalink
  2. Michael wrote:

    Hi Gavin, thanks for your comment.

    I didn’t actually say “Hey, please overwrite the native JS date object”, that’s why you don’t see it.

    In the last listing I put two instances of the `DateProxy` into the context: They just behave like a native JS Date, especially in the accessor (setters as well as some getters), that’s all.

    To achieve that behaviour, you have to combine two proxies: The `ProxyObject` which let me define the behaviour for the accessors and the `ProxyInstant` that has the time behaviour (via the instant). The date wouldn’t work as date should not have a time component (oddly enough, JavaScript and Java of old did this wrong).

    Please let me know if this helps.

    Posted on December 27, 2021 at 8:43 PM | Permalink
  3. Peter wrote:

    I just came across your post here, and had a quick question.

    In you ProxyObject.getMember(String key), you have
    return switch (key) {
    case “getTime” -> (ProxyExecutable) arguments -> delegate.getTime();
    ….
    ….

    Where does “(ProxyExecutable) arguments” come from?

    BTW , I am just getting to know the enhanced switch statements in Java. Cool stuff!

    Posted on March 23, 2022 at 12:57 AM | Permalink
  4. Michael wrote:

    Hi Peter! That is an excellent question.

    I am casting the whole lambda to an instance of https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/proxy/ProxyExecutable.html

    I omitted as much braces as possible, so what’s there is this:

    @Override
    public Object getMember(String key) {
    	return switch (key) {
    		case "getTime" -> (ProxyExecutable) arguments -> delegate.getTime();
    		case "getDate" -> (ProxyExecutable) (arguments -> delegate.getDate());
    		case "setHours" -> new ProxyExecutable() {
    			@Override public Object execute(Value... arguments) {
    				delegate.setHours(arguments[0].asInt());
    				return delegate.getTime();
    			}
    		};
    		case "setDate" -> (ProxyExecutable) arguments -> {
    			delegate.setDate(arguments[0].asInt());
    			return delegate.getTime();
    		};
    		case "toString" -> (ProxyExecutable) arguments -> delegate.toString();
    		default -> throw new UnsupportedOperationException("This date does not support: " + key);
    	};
    }
    
    Posted on March 23, 2022 at 4:57 PM | Permalink
One Trackback/Pingback
  1. Java Weekly, Issue 414 | Baeldung on December 2, 2021 at 6:15 PM

    […] >> Graalvm and Proxy Bindings With Embedded Languages [info.michael-simons.eu] […]

Post a Comment

Your email is never published. We need your name and email address only for verifying a legitimate comment. For more information, a copy of your saved data or a request to delete any data under this address, please send a short notice to michael@simons.ac from the address you used to comment on this entry.
By entering and submitting a comment, wether with or without name or email address, you'll agree that all data you have entered including your IP address will be checked and stored for a limited time by Automattic Inc., 60 29th Street #343, San Francisco, CA 94110-4929, USA. only for the purpose of avoiding spam. You can deny further storage of your data by sending an email to support@wordpress.com, with subject “Deletion of Data stored by Akismet”.
Required fields are marked *