7

I am trying to use Clojure as a scripting language from a host Java program. The idea being that the end user will be able to write Clojure scripting code that will call a domain-specific Java API. At runtime, the host Java program will evaluate the end-user's Clojure script (which will in turn call the domain APIs). So I started with a dead-simple prototype to explore the terrain.

domain

package a.problem.domain;

public class Domain {

    public Domain() { }

    public String defaultMsg() {
        return "default";
    }

    public String passBackMsg(String s) {
        return s;
    }
}

Host Java program

(end user's Clojure script hard-coded for simplicity)

String script = "(do                                    "+
                "  (import '(a.problem.domain Domain))  "+
                "  (.defaultMsg (Domain.))              "+
                ")                                      ";
System.out.println(RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script)));

(code snippet taken from here)

So far so good.

However I couldn't find a way to invoke the second method (the one that requires an argument). Instead I resorted to dynamically generating the Clojure script at runtime and replacing a placeholder with a literal that represents the result of calling the domain method passBackMsg. Obviously, this is unsatisfactory and doesn't go very far (what if I want to pass a java.sql.Connection to my Clojure script ?).

So, how do I invoke the passBackMsg method from the host Java program?

When I try the following:

String script = "(ns foo)                                   "+ 
                "(import '(a.problem.domain Domain))        "+
                "(defn numberToString [s]  (                "+
                "  (.passBackMsg (Domain.) s)               "+
                "))                                         ";
RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script)); // line-A
System.out.println(RT.var("foo", "numberToString").invoke(33)); // line-B

I get:

java.lang.IllegalStateException: Can't change/establish root binding of: *ns* with set

... on line-A. When I try without the ns and with:

RT.var("user", "numberToString").invoke(33)

("user" being a wild guess as I don't see a var method without a namespace argument)

I get an:

java.lang.IllegalStateException: Attempting to call unbound fn: #'user/numberToString"

... on line-B.

5
  • 1
    Couldn't you just defn a function through a script string and then invoke it just like you invoked read-string, by derefing its var? Commented Feb 20, 2013 at 19:10
  • I tried that but I couldn't get it to work. First it would accept defn and had to use (let (fn )) instead. Not a blocking problem but weird. Then I ran into some other problem. I didn't want to copy the failing code so as not to make the post too long, but after your message I might add it. Commented Feb 20, 2013 at 21:25
  • I have this working in a project of mine: clojure.lang.RT.load(filename); clojure.lang.RT.var(mainNamespace, "main").invoke( stringArg ); Loading from a file shouldn't make a fundamental difference. Commented Feb 20, 2013 at 21:37
  • update on defn: it turns out that using a defn was not a problem after all and have updated the script shown, accordingly, the "Can't change/establish root binding of: ns with set" still persists however. Commented Feb 21, 2013 at 7:43
  • You definitely need a do around everything because read-string only reads one form. Also, read-string doesn't evaluate it, it just returns what was read in. This is in contrast to my technique with load. Commented Feb 21, 2013 at 9:07

1 Answer 1

4

Try this:

String script = "(do                                    "+
                "  (import '(a.problem.domain Domain))  "+
                "  (fn [s]                " +
                "   (.passBackMsg (Domain.) s)               "+
                "))                                         ";

IFn fn = (IFn)RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script));

fn.invoke("hello");

UPDATED: Below sample code works fine:

package hello_clj;

import clojure.lang.RT;
import clojure.lang.IFn;

public class Main {

    public String passBackMsg(String s) {
        return s;
    }

    public static void main(String[] args) {
        String script = "(do (import 'hello_clj.Main) (fn [s] " + 
                        "(.passBackMsg (Main.) s) ))";

        IFn fn = (IFn)RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script));
        System.out.print(fn.invoke("Hello"));
    }

}
3
  • It would be more reliable to do it without the import as it may not take effect within the same form as it is being evaluated. Did you try it? Commented Feb 21, 2013 at 9:04
  • java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn (thrown on line-A) Commented Feb 21, 2013 at 9:11
  • Updated the answer with a running sample code. The older code has extra params around fn and that was the issue. Commented Feb 21, 2013 at 9:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.