Skip to content

jgpc42/insn

Repository files navigation

Clojars Project

Dependency and version information

Click to show

⚠️ This library uses a recent version of asm which can cause dependency issues. See here for more.

Leiningen

[insn "0.5.4"]

tools.deps

{insn/insn {:mvn/version "0.5.4"}}

Maven

<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.

What is it?

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.

Quick start

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.

More information

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.

Running the tests

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

Similar libraries

Projects using insn

  • 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.

License

Copyright © 2017-2024 Justin Conklin

Distributed under the Eclipse Public License, the same as Clojure.

About

Functional JVM bytecode generation for Clojure.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Languages