Skip to content

Decompiler API

Mike Strobel edited this page Oct 27, 2019 · 1 revision

The simplest way to integrate with the Procyon Decompiler is through the Decompiler utility class. Unless otherwise noted, all classes described on this page may be found in the com.strobel.decompiler package of the procyon-compilertools library. More elaborate hosting scenarios are not documented at this time.

The Decompiler class provides two decompile() methods:

:::java
public static void decompile(
    final String internalName, 
    final ITextOutput output
);

public static void decompile(
    final String internalName, 
    final ITextOutput output
    final DecompilerSettings settings
);

A single call to either of these methods is required to decompile a class; decompiling multiple classes requires multiple calls.

The first argument, internalName, should be the type descriptor of the class to decompile, e.g., "java/lang/Object". By default, internalName may alternatively be a relative or absolute path to a .class file on disk, though this is dependent on the default settings being used (more on this below).

The output argument will be used used as an output sink by the decompiler. Typically, one will want to capture the plain text output, and for this a PlainTextOutput instance is sufficient. PlainTextOutput wraps a java.io.Writer. By default, it writes to a StringWriter, and the results may be obtained by calling toString() on the output object.

To write to disk, or to any other stream, you may wrap an OutputStreamWriter, but be sure to close() the writer afterwards, or the output may not get flushed:

:::java
final DecompilerSettings settings = DecompilerSettings.javaDefaults();

try (final FileOutputStream stream = new FileOutputStream("path/to/file");
     final OutputStreamWriter writer = new OutputStreamWriter(stream)) {

    Decompiler.decompile(
        "java/lang/String",
        new PlainTextOutput(writer),
        settings
    );
}
catch (final IOException e) {
    // handle error
}

The example above uses Java 7's try-with-resources semantics. If you are using the Java 6 language level, you will need to close() your writer manually:

:::java
try {
    final FileOutputStream stream = new FileOutputStream("path/to/file");

    try {
        final OutputStreamWriter writer = new OutputStreamWriter(stream);

        try {
            Decompiler.decompile(
                "java/lang/String",
                new PlainTextOutput(writer),
                DecompilerSettings.javaDefaults()
            );
        }
        finally {
            writer.close();
        }
    }
    finally {
        stream.close();
    }
}
catch (final IOException e) {
    // handle error
}

DecompilerSettings and ITypeLoader

To use the default settings, simply pass DecompilerSettings.javaDefaults() when calling decompile(), or omit the settings argument altogether. The options governed by DecompilerSettings are mostly straightforward and roughly map to the command-line options supported by the decompiler front-end.

One member which requires some explanation is setTypeLoader(). This method specifies which ITypeLoader should be used to resolve and load .class files. The interface has one method:

:::java
public boolean tryLoadType(final String internalName, final Buffer buffer)

Before a class can be decompiled, its .class file must be found, loaded, and parsed. This method is responsible for finding the file and loading it into the provided buffer. Throughout decompilation, many other classes will be loaded as well, many of which will be part of the core JRE. While these classes may not be fully decompiled, the decompiler will need to inspect any classes referenced by a decompiled class in order to determine things like inheritance hierarchies and method overloads. Therefore, the ITypeLoader provided should be capable of resolving any referenced classes, as well as their super classes and super interfaces. If the decompiler cannot load and analyze referenced classes, the results may be sub-optimal, e.g., they may contain redundant casts and less specific typing than they would otherwise.

By default, an InputTypeLoader is used. InputTypeLoader is the type loader used to handle the input passed to the command-line decompiler, and it is the most flexible of the type loaders: it allows a relative or absolute .class file path to be substituted for internalName, and dotted names to be used in lieu of type descriptors (e.g., java.lang.String instead of java/lang/String). When locating classes by name or type descriptor, an InputTypeLoader falls back to a wrapped defaultTypeLoader. Unless otherwise specified, the default type loader is a ClasspathTypeLoader, which, as the name suggests, uses standard classpath search semantics.

The behavior of InputTypeLoader is as follows:

  1. If internalName is a path to a .class file, it will attempt to load that file directly. If found, the type loader will make a note of the file's package name and location on disk. It may use this information to help locate classes with similar package names in the future, e.g., when the decompiler attempts to load referenced classes.

  2. If internalName was an absolute/rooted path to a .class file, and step 1 failed, then the type loading fails.

  3. If present, the .class file extension will be removed, any '.' characters will be converted to '/', and the result will be treated as a type descriptor.

  4. The package name will be extracted from the type descriptor, and the type loader will attempt to locate the class file in any of the previously recorded search paths from step 1.

  5. Finally, if the type has still not been loaded, the type descriptor will be passed to the defaultTypeLoader. If loaded successfully, the method returns.

  6. If the class was not located, the type loader will attempt an inner class search by replacing one / character at a time with $, from right to left, and repeating steps 4 and 5 each time.

InputTypeLoader is meant to be flexible enough overriding the default ITypeLoader should be unnecessary. However, some scenarios like loading a class file directly from a byte[] array may require it. Several ITypeLoader implementations are provided, which may be combined together as necessary to achieve the desired results:

  • InputTypeLoader functions as described above.

  • ClasspathTypeLoader uses standard classpath-based search semantics. Unless otherwise specified, the classpath searched will be the union of the java.class.path and sun.boot.class.path properties.

  • JarTypeLoader loads classes from within a .jar file.

  • ArrayTypeLoader wraps a byte[] array containing a single classfile structure. It is capable of loading only the type located within the array, and fails on all other types.

  • CompositeTypeLoader wraps multiple type loaders, and will attempt to load a class using each one, in order, until one of them succeeds.

If you do end up overriding the default ITypeLoader, it is recommended that you use a CompositeTypeLoader with an InputTypeLoader as the last loader in the chain.

Note that with the exception of InputTypeLoader, each of the ITypeLoader implementations above requires internalName to be an exact type descriptor. CompositeTypeLoader does not care what internalName contains, as it merely passes it on to the wrapped type loaders.

Clone this wiki locally