Search code examples
graalvm

GraalVM - Using Polyglot Value without a context


I am writing an application ontop of Graal that will be able to execute small scripts in different languages.

I am trying to write some unit tests for a class I am using to convert/process the result of a Context.eval() call (type: Value) to a Java object. I know from the documentation that a Value instance is always bound to a Context, so when I try to write something like this:

@Test
public void NumericFloatTest() throws ScriptExecutionException {

    GuestLanguageResultProcessor LangProcessor = new GuestLanguageResultProcessor();
    Float javaValue = (float) 43.25;
    Value numValue = Value.asValue(javaValue);
    LangProcessor.processResult(numValue);

    Object result = LangProcessor.processResult(numValue);

    assertThat(result.getClass()).isEqualTo(Float.class);
}

I get the following error:

java.lang.IllegalStateException: No current context is available. Make sure the Java method is invoked by a Graal guest language or a context is entered using Context.enter().

I guess that conceptually it does not make sense to have a "Value" instance without an associated bit of guest code, so my question is:

How can I go about testing my GuestLanguageResultProcessor class? Do I have to "bloat" my unit test with the creation of a context?

Side question for the experts: I am using this class (GuestLanguageResultProcessor) also to extract a Java value from the polyglot Value instance so that I can close the context. In other words it looks to me that before being able to do Context.close() I need to call [value instance].asString() or .asWhatever() in order to get the result out and being able to close the context without getting an IllegalStateException as it says in the docs.

Am I doing it right? Is there a better way to handle getting the result and closing the context safely?

Thank you!


Solution

  • How can I go about testing my GuestLanguageResultProcessor class? Do I have to "bloat" my unit test with the creation of a context?

    I fear a bit of bloating is necessary. I'd recommend to use the following code to make your test work. This can also be done in a test base class, to avoid repetition.

    Context context;
    
    @Before
    public void setup() {
        context = Context.create();
        context.enter();
    }
    
    @After
    public void setup() {
        context.leave();
        context.close();
    }
    
    @Test
    public void NumericFloatTest() throws ScriptExecutionException {
        GuestLanguageResultProcessor LangProcessor = new GuestLanguageResultProcessor();
        Float javaValue = (float) 43.25;
        Value numValue = Value.asValue(javaValue);
        LangProcessor.processResult(numValue);
    
        Object result = LangProcessor.processResult(numValue);
    
        assertThat(result.getClass()).isEqualTo(Float.class);
    }
    

    Am I doing it right? Is there a better way to handle getting the result and closing the context safely?

    Value instances may be bound to guest language objects, like JavaScript objects which are invalid as soon as their context is closed. It is not always possible to convert guest language objects to a permanent Java representation. For example polyglot values might refer to an entire graph of JavaScript objects.

    If possible, I would recommend to keep the context open as long as values are needed as it does not require any conversions.

    If that is not possible and you are only dealing with primitives and Arrays you can try using the following method. You may also try to copy objects into Java land by accessing its members.

    Object copyToJavaLand(Value value) {
        if (value.isBoolean()) {
            return value.asBoolean();
        } else if (value.isString()) {
            return value.asString();
        } else if (value.isNumber()) {
            return value.as(Number.class);
        } else if (value.isHostObject()) {
            return value.asHostObject();
        } else if (value.isProxyObject()) {
            return value.asProxyObject();
        } else if (value.hasArrayElements()) {
            Object[] array = new Object[(int) value.getArraySize()];
            for (int i = 0; i < array.length; i++) {
                array[i] = copyToJavaLand(value.getArrayElement(i));
            }
            return array;
        }
        throw new IllegalArgumentException("Cannot copy value " + value + ".");
    }
    

    Please note that this method is not always safe. For example if arrays refer to themselves, this method would crash with a stack-overflow error.