Skip to content

Mixin 0.8.3

Compare
Choose a tag to compare
@Mumfrey Mumfrey released this 01 Jul 17:21
· 85 commits to master since this release

Despite being only a point release, Mixin 0.8.3 packs a decent punch in terms of bundling up a lot of bug fixes for a respectable number of issues, as well as laying the groundwork for some upcoming features and freezing the contract of some internal APIs so they can be safely exposed to mixin authors. Among the fixes are some (yes more) small but significant improvements to the way local variables are computed, which makes them much more robust than ever before.

Also clarified is the general contract of the compatibilityLevel behaviour in Mixin, which has been a sticking point for adopting newer Java versions, see below for more details on that.

Features

Target Selector Improvements

With the introduction of Mixin 0.8, I abstracted much of the interface for the venerable MemberInfo into a new more generalised target selector in order to make the entire system of querying for targets more pluggable. Mixin 0.8.3 sees this almost to completion, finalising and freezing the contract of ITargetSelector, and introducing registration mechanics similar to those for InjectionPoints.

Mixin configs can now specify a dynamicSelectors collection, with the names of classes implementing ITargetSelectorDynamic. Selectors declared in this way should be decorated with @SelectorId similarly to the way that custom injection points are decorated with @AtCode.

Dynamic target selectors are specified in the selector string but have a simple recognisable format:

// Use of a built-in custom selector
target = "@SelectorId(custom,argument,string,in,any,format)"

In order to prevent accidental overlaps, mixin configs should now specify a namespace for their custom selectors and injection points, thus a user-provided dynamic selector is used like this:

// Use of a user-provided custom selector
target = "@Namespace:SelectorId(some arguments)"

Namespaces and selector IDs are case-insensitive.

New Built-In Dynamic Target Selectors

With the introduction of dynamic selectors, two new built-in selectors are now available:

  • MemberMatcher (javadoc) introduces a new syntax for matching members via regular expressions. Since these regular expressions are not remapped, this is primarily of use to products mixing into non-obfuscated platforms which change often. However with some creativity they can be leveraged to great effect on obfuscated platforms too, providing a similar mechanic to aliases (eg. /(devName|obfName)/).
  • DynamicSelectorDesc (javadoc) introduces @Desc selectors, more on these later once support in the toolchain matures.

User-Defined Classes in Mixin Packages

In 0.8.3 it is now possible to declare custom classes (Injection Points and Target Selectors) inside of mixin packages. This allows one-use custom injection points to be more neatly packaged with the mixin which consumes them for example.

Changes to Service Bootstrap

It was previously necessary for third-party services to camp in one of the mixin packages in order to instatiate the transformer chain. Since the Mixin binaries are digitally signed this was a problem for third-party services since they needed to strip the signer information from the mixin jar in order to inject their service.

In Mixin 0.8.3, service startup has been reworked so that services are now offered a one-use IMixinTransformerFactory via the new internals mechanism. (This approach chosen so that other internal objects can be passed easily in the future).

Enhancements to MemberInfo

It wouldn't be fair for all this new stuff to be happening without the trusty MemberInfo getting some light buffs. Since the new ITargetSelector supports both a minMatchCount and a maxMatchCount, new syntax for MemberInfo now extends the previous * (match all) syntax to support declaring min and max values using a similar format to regex quantifiers. Example quantifiers might be {3} (match exactly 3), {2,} (match at least 2), {1,6} (match 1 to 6 (inclusive)). The javadoc for MemberInfo has more examples.

About compatibilityLevel in Mixins

Mixin 0.8.3 fixes a bug in the handling of compatibility level which has caused confusion because nobody realised it was a bug. Whilst I have spoken about its role in the past, the language isn't particularly clear about what the level should be, other than it needing to be "set to the highest level required by your mixins" without clarifying what "highest level" means.

Much like minVersion, the compatibilityLevel declaration is meant as a safeguard against consuming a mixin which uses certain features of the JVM, that mixin must understand and process, thus just choosing the highest level available simply removes this safeguard. The reason the "must understand and process" qualification is important is that Java as a language introduces new features all the time, but often these don't require any special handling on Mixin's part. For example Java could introduce the elvis operator tomorrow, and it wouldn't require any additional bytecode to do so, it's simply syntactic sugar for a compiler feature.

New features such as private methods in interfaces, dynamic constants and nesting are bytecode-level features that do require support in Mixin, and therefore knowing the compatibility needs to be high enough to support those features is an important distinction, since if those features exist in the mixins being consumed, then Mixin has to process and merge them.

The source of the confusion has stemmed from two places, largely because a lot of this stuff was designed when Sponge was the only project using Mixin and these concepts were understood and never written down explicitly anywhere:

  • Firstly, the CompatibilityLevel enum declares java levels above the highest supported by Mixin (previously JAVA_9) with no formal declaration anywhere that these values don't necessarily indicate supported versions.

  • Secondly, the check in the code which validated the class version in use came with the implicit assumption that mixin authors would know which versions of Java were supported by mixin, and would set the source compatibility of their mixin SourceSet to the version they needed. This assumption existed for so long that it was largely forgotten, since JAVA_8 has been the de-facto highest version in use for such a long time. The class version check was therefore sufficient and didn't provide any more information when the check was violated, simply advocating raising the level.

In order to address these issues, and make compatibilityLevel meaningful again, the naïve check has now been replaced with a much more intelligent heuristic, designed to check in a useful way whether the language features required are actually supported by the current level, and encourage mixin authors to elevate to the lowest level which supports their required feature set. Since Mixin doesn't fully support features for any version beyond Java 11 at the moment, this has also been encoded so that a warning can be emitted when using compatibility levels higher than this.

As of Mixin 0.8.3, the current compatibility matrix is as follows, where ✔ indicates a feature is fully supported by Mixin, and ✖ indicates a feature is supported by the runtime but not yet supported by Mixin.

Feature JAVA_6 JAVA_7 JAVA_8 JAVA_9 JAVA_10 JAVA_11
INVOKEDYNAMIC
METHODS_IN_INTERFACES
Non-abstract default methods in interfaces
PRIVATE_SYNTHETIC_METHODS_IN_INTERFACES
Private synthetic methods (eg. lambda bodies) allowed but user-defined private methods prohibited
DYNAMIC_CONSTANTS Partial
PRIVATE_METHODS_IN_INTERFACES
User-defined private methods
NESTING

Partial support for DYNAMIC_CONSTANTS is shown because Mixin will attempt to remap dynamic constants appearing in mixins, but this functionality has not been tested as I haven't yet encountered any dynamic constants in the wild.

But then why not always just set the compatibilityLevel to the highest possible value?

As explained in the PSA linked above it may at some point be necessary to support new features by breaking support for old ones. Thus the current range of supported compatibility levels is actually a moving window rather than a guaranteed "this level or below" declaration. For example let's assume that a feature added to support Java 17 has to break compatibility with Java 8. Now you have an either/or behaviour where mixins targetting those versions can't co-exist in the same runtime.

The correct approach is always to target the lowest version which satisfies the required language features for your mixins, preserving as much backward compatibility as possible with other mixins.

If you have any questions about compatibility level, please feel free to join #mixin on the Sponge Discord to raise any queries you may have.

Bug Fixes and Improvements

  • Decompilation is now supported with current builds of Fernflower as well as ForgeFlower. The old version is still supported.
  • Mixin configs can now omit the package declaration if their only purpose is to be used as a commmon parent for other configs
  • Fixes and improvements to computing method locals in a whole bunch of situations, in particular some issues with double-slot variables (longs and doubles)
  • Improved computing locals when the LVT is missing or incomplete, as well as better detection of variables which go out of scope on or immediately before the selected instruction
  • Fixed a bug which prevented nullValue from working correctly with the CONSTANT injection point
  • Merged changes from @SizableShrimp to support the new TSRGv2 format
  • Added a quiet option to the annotation processor to suppress banner and status messages in the console
  • Many improvements to the javadoc, including more detail for some injector types and a new visual theme
  • Fixed an issue with static invokers when the staticness of the invoker itself did not match the target
  • Shifting beyond the method bounds with At.shift no longer errors
  • Invokers are now correctly decorated with their target, fixing the "Undecorated Accessor" error
  • Custom injectors are now resolved correctly as the resolver doesn't use endsWith(simpleName) internally any more
  • Fixed code which strips (TargetClass) (Object) this double-casts so that it doesn't cause corrupted code when the method is static
  • Made AP resolver for inner classes more aggressive so that anonymous inner class targets are resolved properly in more cases

What's Next

The next version of Mixin will be 0.8.4, with a paricular focus on finalising support for Java up to 11, providing basic support for up to Java 16, and adapting to upcoming changes in ModLauncher. Watch this space.

Join the discussion in #mixin on the Sponge Discord.