Click to show
⚠️ This library uses a recent version ofasm
which can cause dependency issues. See here for more.
[insn "0.5.4"]
{insn/insn {:mvn/version "0.5.4"}}
<dependency>
<groupId>insn</groupId>
<artifactId>insn</artifactId>
<version>0.5.4</version>
</dependency>
LTS JDK versions 8, 11, 17 and 21 and Clojure versions 1.7 to 1.12 are currently tested against.
This library provides a functional abstraction over ASM for generating JVM bytecode. ASM is the library that Clojure itself uses to dynamically compile Clojure code into code that can be run on the JVM.
Let's begin by creating a simple class, equivalent to the following Java code.
package my.pkg;
public class Adder {
public static int VALUE = 42;
public long add (int n) {
return (long) (VALUE + n);
}
}
The class is specified as a map. The class fields and methods are sequences of maps giving the members of said class.
(def class-data
{:name 'my.pkg.Adder
:fields [{:flags #{:public :static}, :name "VALUE", :type :int, :value 42}]
:methods [{:flags #{:public}, :name "add", :desc [:int :long]
:emit [[:getstatic :this "VALUE" :int]
[:iload 1]
[:iadd]
[:i2l]
[:lreturn]]}]})
Above, we described in data the same information expressed by the Java code, except the method body was given as a sequence of bytecode instructions. (Note: unlike Java, the method return value is specified via the :desc
key as the last element). If you aren't fluent in JVM bytecode instruction syntax, I would suggest reading chapter 3 of the excellent tutorial pdf from the ASM site.
:emit
can also be a fn that is passed the ASM MethodVisitor
object to write the method bytecode as shown in this example.
Now to write the bytecode.
(require '[insn.core :as insn])
(def result (insn/visit class-data))
The result
is a map containing the generated class's packaged-prefixed :name
and :bytes
, the latter being a byte array. This information is all you need to give to a ClassLoader to define your class.
For convenience, we can use insn.core/define
to define the class for us.
(def class-object (insn/define class-data)) ;; => my.pkg.Adder
(-> class-object .newInstance (.add 17)) ;; => 59
Note that you can also pass result
to define
, the class will not be regenerated. Also note, like Java, since we did not define any constructors, a public no-argument constructor that simply calls the superclass constructor was generated for us.
If you are evaluating the code snippets above in the REPL, you can also just do:
(.add (my.pkg.Adder.) 17) ;; => 59
Since, by default, define
loads the class using Clojure's own DynamicClassLoader
, meaning the class will be first class to subsequent evaluations in the running Clojure environment.
For additional usage examples and topics, see the wiki. For a complete reference, see the docs. The fairly comprehensive test suite is also demonstrative and should be easy to follow.
lein test
Or, lein test-all
for all supported Clojure versions.
The tests can also be run against all supported Java versions (via docker
) with:
./test-all-jdk.sh
- tools.emitter.jvm
- Does not provide a general ASM API.
- mage and magic
- Clojure-CLR only.
- tech.datatype
- Efficient N-dimensional numerics across a range of primitive datatypes and containers.
- Also used by this libraries' successor, dtype-next.
- jmh-clojure
- Clojure bridge to JMH benchmarking via bytecode generation.
Copyright © 2017-2024 Justin Conklin
Distributed under the Eclipse Public License, the same as Clojure.