Skip to content

Features

ME1312 edited this page Oct 15, 2024 · 23 revisions

Bridge is a post-compile maven plugin that injects new advanced functionality into the Java language using existing semantics. Currently, we add the following features:

Javadocs for specific code elements discussed on this page can be found here.

Bridges

If you've ever wanted to change something, but couldn't allow yourself to do so for signature-compatibility reasons, then what you needed at that moment was a bridge. The @Bridge annotation allows you to quickly and easily create synthetic redirects for constructors, methods, and fields; with support for automatic type conversion and other advanced options.

Fields

@Bridge(name = "oldField")
public Example newField;

This is enough to create a simple field bridge. The main limitation with these is that updating newField outside of constructors does not also update oldField, making them perhaps more suited for use with final fields.

@Bridge(returns = OldType.class)
public final NewType field;

Also of note is that, unlike standard Java, @Bridge will allow you to create multiple fields that share the same name, as long as their types differ.

Methods

@Bridge(returns = OldType.class)
public NewType someMethod() {
    // -implementation-
}

This is enough to create a simple method bridge.

@Bridge(params = {Object.class, OldType.class})
public void someMethod(Object same, NewType updated, long newParam) {
    // -implementation-
}

@Bridge can also convert the index, amount, and type of arguments that methods and constructors take in. The main limitation here is that arguments can not be rearranged; they can only be presented in the order in which they were received.

Invocations

If you've known that you had access to something, but had to use reflection anyway because that something was out of scope, then you could have used an invocation. Invocations are reflection-like code that get recompiled to native instructions; with support for checked exception handling, automatic type detection, and type conversion.

Classes

Class Literal Invocation Resulting Bytecode
new Invocation("com.example.Example").ofClassLiteral()

LDC

Instance-Of Invocation
new Invocation(object).ofInstanceOf("com.example.Example")    

(object)
INSTANCEOF

This is the same as typing Example.class and (object instanceof Example), respectively.

Constructors

Constructor Invocation Resulting Bytecode
new Invocation("com.example.Example").ofConstructor()
        .with(CharSequence.class, "Hello world!")
        .with((int) 1337)
        .invoke()

NEW
DUP
(arguments & conversions)
INVOKESPECIAL

Array Constructor Invocation
new Invocation("[[Lcom.example.Example;").ofConstructor()    
        .with(6)
        .with(4)
        .invoke()

(array dimension lengths)
MULTIANEWARRAY

This is the same as typing new Example("Hello world!", 1337) and new Example[6][4], respectively.

Methods

Static Method Invocation Resulting Bytecode
new Invocation("com.example.Example").ofMethod(void.class, "method")
        .with(CharSequence.class, "Hello world!")
        .with((int) 1337)
        .invoke()

(arguments & conversions)
INVOKESTATIC

Virtual Method Invocation
new Invocation("com.example.Example", instance).ofMethod(void.class, "method")    
        .with(CharSequence.class, "Hello world!")
        .with((int) 1337)
        .invoke()

(instance)
(conversions)
(arguments & conversions)
INVOKEVIRTUAL

These are the same as typing .method("Hello world!", 1337).

Fields

For the sake of clarity and brevity, this section will omit the virtual variants of these functions – but, they do exist.
The difference in syntax between the two is the same as in the above section.

Field Get Invocation Resulting Bytecode
new Invocation("com.example.Example").ofField(Object.class, "field").get()

GETSTATIC

Field Get-and-Set Invocation
new Invocation("com.example.Example").ofField(Object.class, "field").getAndSet(value)    

GETSTATIC
(value)
(conversions)
PUTSTATIC

Field Set Invocation
new Invocation("com.example.Example").ofField(Object.class, "field").set(value)

(value)
DUP
(conversions)
PUTSTATIC

Field Set-and-Get Invocation
new Invocation("com.example.Example").ofField(Object.class, "field").setAndGet(value)    

(value)
(conversions)
DUP
PUTSTATIC

Potential bytecode output within these sections was provided to illustrate the subtle, but important, differences between these calls. In reality, individual instructions may be omitted or swapped for more appropriate ones to match the context of the call; providing greater efficiency and compatibility. For more information about the instructions themselves, see the Java Virtual Machine Specification.

Jumps

The Java Language Standard reserved the keyword goto all they way back in 1.0... but then refused to implement it for the sake of readability. I personally don't even really disagree with that sentiment, but sometimes, you just need it. If you know, you know. So, that functionality ended up here. Jump provides an actual implementation in the absence of, and supplants, the abandoned goto keyword.

// in a method somewhere
new Label("example");
// do something
throw new Jump("example");

It's that easy to jump to a label... but, the keen-eyed will notice that this example, when left unaltered, creates an infinite loop. With great power, comes great responsibility.

if (condition) throw new Jump("example");
// any code here is skipped when (condition == true)
new Label("example");
// method continues

You can jump forwards, backwards, in, and out of if statements and loops. The only restriction is that your Jump must be in the same lambda/method as the corresponding Label.

Forks

Multi-release jar files were introduced in Java 9, and they're great for allowing you to maintain the backwards-compatibility that java users know and love, but, well... having to maintain several versions of the same source files just to make use of modern API functionality makes actually targeting multiple versions a rather painful experience. Forget about forgetting to update the nth version of a class buried in META-INF and let the recompiler fork it for you.

if (Invocation.LANGUAGE_LEVEL >= 9) {
    System.out.println("This statement only appears in Java 9+");
}

Like this, you can derive multiple classes with version-specific code from just one source file. The recompiler will detect, remove, and perform the comparison itself to generate the required forks.

return (Invocation.LANGUAGE_LEVEL > 18)? Objects.toIdentityString(object) : object.toString();

This tactic can be used pretty much anywhere a boolean expression is evaluated, and supports all forms of integer constant comparison. The only restrictions here are that LANGUAGE_LEVEL must be the first operand in the expression, and that you may only fork to supported language levels above the one used to compile your class.

Unchecked

Java supports both checked and unchecked exceptions. This functionality is limited, however, by the fact that you are required to handle all checked exceptions that may occur within your method or add them to your method's signature – forcing any downstream developer using that method to make the same choice yet again. It's a system designed with good intentions, but it can be annoying at times... which is why we, too, are providing a way around this.

throw new Unchecked(new ExampleException());

Yep, you can simply declare any instance of an exception unchecked when you throw. Unlike other solutions, this isn't a wrapper, and there's no method calls required under the hood. It just works.

try {
    // some code that might throw ExampleException
} catch (ExampleException e) {
    throw Unchecked.boomerang();
}

If rethrowing exceptions isn't your style, then try boomeranging them instead. The listed exceptions will have their catch blocks removed until the try block itself can be removed, allowing the exceptions within to propagate naturally from the location at which they were originally thrown.

try {
    Unchecked.<ExampleException>check();
    // some code that might throw ExampleException
} catch (ExampleException e) {
    // exception handling
}

We also provide a way for you to bring those unchecked checked exceptions back into scope. These, too, will disappear from your code after it compiles, leaving behind only the code with exceptions you want to handle.

// suppose this exists somewhere
LinkedList<Generic<Type<Disaster<?>>>> complex = new LinkedList<>();
// but what you need is this
List<Generic<?>> simple = Unchecked.cast(complex);

And, if you find yourself in need of simplified unchecked casting when working with other types, we've got you covered there, too. These leave behind only the code required to make the conversion, which for the above example, is nothing.

Type Adoption

This is an advanced feature that allows a class to @Adopt new parent classes and attributes post-compilation. You'll likely end up using it alongside @Bridge to resolve generic type disputes when extending or implementing classes and interfaces.

@Adopt(parent = SomeClass.class, interfaces = { InterfaceA.class, InterfaceB.class, InterfaceC.class })
public class MergedType {
    // -implementation-
}

Naturally, if you fail to implement a method in the end, that method won't work when called. In addition to that, though, whatever class you choose to extend must have accessible constructors available that are exact matches to what may have been specified in your super() calls. If you didn't specify super(), then that would be the default zero-argument constructor.

Appending the Synthetic Modifier

Java compilers use the synthetic modifier to mark automatically generated elements that downstream developers don't necessarily need to see. With the @Synthetic annotation, you can do the same for any element. This is then used by IDEs to determine what to show downstream developers.

@Synthetic
public static Type sneakyMethod() {
    // -implementation-
}

Given its simplicity, this works across classes, constructors, methods, and fields. It is not recommended to use this with the purpose of achieving "security through obscurity," however, as this will not protect you from invocations and reflection.

Removing Debug Metadata

Last, but not least, this section describes the flags section of the plugin configuration. Specify as little or as many of these as you need to get the output you desire. As the section title implies, most of these are indeed for managing metacode.

<configuration>
    <flags>
        <flag>skip-compile</flag>  <!-- disables the recompiler -->
        <flag>force-compile</flag> <!-- recompile even if no changes are detected -->
        <flag>no-debug</flag>      <!-- shorthand for all of the following flags -->
        <flag>no-source</flag>         <!-- shorthand for the following two flags -->
        <flag>no-source-names</flag>   <!-- ==> removes source file names from classes -->
        <flag>no-source-ext</flag>     <!-- ==> removes source file extension data from classes -->
        <flag>no-module-versions</flag><!-- removes module version from module-info.class -->
        <flag>no-named-params</flag>   <!-- removes parameter names in methods -->
        <flag>no-named-locals</flag>   <!-- removes variable names in methods -->
        <flag>no-line-numbers</flag>   <!-- removes line numbers in methods -->
    </flags>
</configuration>





Clone this wiki locally