Clojure/BoxLang interop

I’ve been experimenting with the (much improved) Java interop in BoxLang and wanted to see how much I could simplify my cfmljure project. Here’s what I ended up with:

My Application.bx file:

import java.lang.Class;
import java.lang.reflect.Array;
import java.lang.Thread;

class {
  this.javaSettings = {
    loadPaths: systemExecute("clojure","-Spath").output.split(":")
  };

  function onApplicationStart() {
    var urls = this.javaSettings.loadPaths.map(p =>
      new java:java.io.File(p).toURI().toURL()
    );
    // replace the classloader with all the necessary URLs added:
    var myThread = Thread::currentThread();
    var classloader = new java:java.net.URLClassLoader(
        urls.toArray( Array::newInstance( Class::forName("java.net.URL"), urls.size() ) ),
        myThread.getContextClassLoader()
    );
    myThread.setContextClassLoader( classloader );

    var stringType = Class::forName("java.lang.String");
    var objectType = Class::forName("java.lang.Object");

    // install clojure.java.api.Clojure:
    var clj = classloader.loadClass("clojure.java.api.Clojure");
    // Object var(Object ns, Object name):
    var _var = clj.getMethod("var", [objectType, objectType]);
    // Object read(String s):
    var _read = clj.getMethod("read", [stringType]);
    // BoxLang wrapper:
    application.clj = {
      var: (ns, name) => _var.invoke(null, [ns, name]),
      read: (s) => _read.invoke(null, [s])
    };

    // expose for debugging only:
    application.paths = this.javaSettings.loadPaths
    application.urls = urls;
  }

}

And the index.bxs file:

// verify classpath from Clojure:
for (p in application.urls) echo(p & char(10))

for (x in application.clj.read("[1 2 3]")) echo(x & char(10))

plus = application.clj.var("clojure.core", "+")
echo( "(+ 1 2) = " & plus.invoke(1, 2) & char(10) )

Running this (with Beta 20):

sean@sean-win11-desk:~/boxlang$ bin/boxlang index.bxs 
file:/home/sean/boxlang/src
file:/home/sean/.m2/repository/org/clojure/clojure/1.12.0/clojure-1.12.0.jar
file:/home/sean/.m2/repository/org/clojure/core.specs.alpha/0.4.74/core.specs.alpha-0.4.74.jar
file:/home/sean/.m2/repository/org/clojure/spec.alpha/0.5.238/spec.alpha-0.5.238.jar
1
2
3
(+ 1 2) = 3

I had hoped that just setting the classpath from the result of clojure -Spath would be sufficient to provide the correct classpath for Clojure execution, but I wasn’t able to get it to work (I think this is due to some static initialization that Clojure does).

If it had worked, index.bxs would look more like this:

import clojure.java.api.Clojure;

for (x in Clojure::read("[1 2 3]")) echo (x & char(10))

plus = Clojure::var("clojure.core", "+")
echo( "(+ 1 2) = " & plus.invoke(1, 2) & char(10) )

Note: this doesn’t fully replicate cfmljure because it doesn’t have the onMissingMethod() adapter for auto-requiring/installing Clojure namespaces on first invocation but for basic interop, it’s already pretty good – and better than the original CFML code.

I was a bit surprised/disappointed that the following did not work:

var classloader = Thread::currentThread().getContextClassLoader();

This is rejected with:

ortus.boxlang.runtime.types.exceptions.ExpressionException: Unsupported access type: ortus.boxlang.compiler.ast.expression.BoxMethodInvocation
 position:/home/sean/boxlang/index.bxs: 9,28 - 9,52
 sourceText: .getContextClassLoader()
        ortus.boxlang.compiler.javaboxpiler.transformer.expression.BoxStaticAccessTransformer.transform(BoxStaticAccessTransformer.java:55)
        ortus.boxlang.compiler.javaboxpiler.JavaTranspiler.transform(JavaTranspiler.java:283)

It would be nice to be able to chain method calls on static method calls.

In case anyone else wants to play with this, I’ve put a slightly updated version on GitHub: seancorfield/boxlang-clojure: An example of calling Clojure code from BoxLang

This was definitely a bug in the parser. I have fixed it in [BL-711] - Welcome

Can you let us know how to reproduce this so @lmajano can look at the classloading?

Thanks for BL-711 – that will allow me to simplify the code.

Re: classpath and repro. I’ll add some prose and code examples to that repo to show what I expected to happen but, essentially, I didn’t expect any of the onApplicationStart() code to be needed, and I wrote what I had hoped would work: importing the Clojure API class and then calling its static methods as shown.

1 Like

I have updated the README to explain what I expected, and added simple.bxs to show the sort of code I hoped would work – without all that onApplicationStart() code.

1 Like

I have updated the repo in light of Beta 21 addressing all the issues I was having with classloaders.

Thank you, BoxLang team!