The Cojen/Maker library is a lightweight, full-featured, low-level dynamic Java class generator which is designed for ease of use.
Here's a simple "hello, world" example:
ClassMaker cm = ClassMaker.begin().public_();
// public static void run()...
MethodMaker mm = cm.addMethod(null, "run").public_().static_();
// System.out.println(...
mm.var(System.class).field("out").invoke("println", "hello, world");
Class<?> clazz = cm.finish();
clazz.getMethod("run").invoke(null);
A key feature of the library is that the JVM operand stack isn't directly accessible, which makes it much easier to use. Local variables are used exclusively, and conversion to the stack-based representation is automatic. In some cases, this can result in more local variables than is strictly necessary, but modern JVMs reduce the set using liveness analysis.
In addition to simplifying basic class generation, the features of the java.lang.invoke
package are fully integrated, but without all the complexity. The ObjectMethods
example shows how to define a bootstrap method which generates code "just in time".
The API is designed to be very simple, defined by a few interfaces and no public classes. Despite its tiny size, the API supports nearly all of the JVM features and bytecode instructions. Features like generics aren't directly supported, although due to type erasure, dynamically generated classes don't require generics anyhow. Instructions which directly manipulate stack operands aren't supported either, because such access isn't necessary with this API.
In order to keep the API simple, some capabilities are merged into common interfaces. In particular, there's no Type
class to represent things like variable types. Instead, a plain Class
can be used to specify a type, as can a string name or descriptor. To reference types which are currently being made, a ClassMaker
instance can be used as a type. Variable
instances themselves can be used as generic type specifiers, and this doesn't necessarily create an actual variable in the generated bytecode.
Java bytecode is imperative in nature, and for this reason, the API is also imperative. Code execution order is top to bottom, and labels are used to control execution flow. Higher-level scoping features are provided in a few cases as a convenience. In particular, exception handlers can be generated with a callback, eliminating the need to specify an explicit goto to skip past the handler. This is even more helpful when generating finally
clauses, as it ensures that all exit paths from a guarded scope are properly covered.
Type conversions between primitive types are performed automatically, including boxing/unboxing conversions. Narrowing conversions aren't performed automatically, unless it can be proven that this causes no loss of information. This effectively limits such conversions to constants only.
Finished classes can be loaded immediately, or they can be written out to a file. Classes which are immediately loaded are eligible to be unloaded when all generated classes in the group are no longer referenced. In this context, a "group" is defined by a parent ClassLoader
and an optional key object. The group itself is a child ClassLoader
, and so classes in the group have package-level access to each other. Applications can control permissions and unloading behavior by carefully choosing an appropriate key object. Classes can also be defined as hidden, in which case they can be unloaded even when the group itself cannot be unloaded.
The java.lang.invoke
package provides powerful features for dynamically generating code, but it's quite complicated and somewhat incomplete. Nonetheless, it does provide very useful features for supporting the so called invokedynamic
instruction. The Cojen/Maker API fully supports these features, and it does so seamlessly. MethodHandle
and VarHandle
instances can be freely exchanged at various points in the API, at code generation time or at runtime. To use the invokedynamic
instruction, the API provides indy
and condy
methods for specifying the required bootstrap method. Any kind of constant can be passed into the bootstrap method, because special encoding strategies are performed automatically, including the handling of Constable
objects.
Another nice feature is the setExact
method, which allows arbitrary object instances to be passed into dynamically generated classes. Ordinarily, only simple constants can be specified, or else the "condy" feature must be used to reconstruct the object upon demand. Underneath the covers, setExact
uses the condy feature to extract the object instance from a special hash table, keyed by the class instance itself. This feature doesn't work for generated classes which are loaded from a file.
The implementation of the Cojen/Maker library is relatively small, and it has no dependencies. The release jar size is ~248KiB, which includes all debugging information.
The Cojen/Maker library is designed for implementing dynamic languages, and for designing utilities that achieve higher performance than is possible when using the reflection API. It isn't designed for modifying classes or for implementing instrumentation agents. That is, you cannot start with an existing class and make modifications to it — classes are only ever made "from scratch". A future version might support class modifications, but there's no plans at this time.
Although the library can be used for writing a frontend compiler, it doesn't have any facilities for reading class symbols. For example, it's possible to write a Java compiler that uses Cojen for writing class files, but it would need another tool for extracting symbols from pre-compiled jar files and so forth. Such a feature could be added of course, but it's a lower priority.
Because of its somewhat low-level design, the library doesn't prevent the creation of broken classes. For example, failing to definitely assign a value to a variable will cause a VerifyError
to be thrown when loading the class. The Coding errors page has more details.