Skip to content

Instantly share code, notes, and snippets.

@LlamaLad7
Last active April 23, 2024 06:18
Show Gist options
  • Save LlamaLad7/ac2f4ced03d21afddde80c5c5e9b302f to your computer and use it in GitHub Desktop.
Save LlamaLad7/ac2f4ced03d21afddde80c5c5e9b302f to your computer and use it in GitHub Desktop.
Expressions docs

Expressions allow you to use java-like strings to target complex pieces of bytecode. Let's have a look at an example first of all, and more thorough documentation will follow:

Example

I suggest reading the ModifyExpressionValue page first.

When targeting code such as the following:

if (this.fallDistance > 0.0F) {  
    doSomething();
    doSomeOtherThing();
}

you may wish to add your own check in the if condition.

This could be done like so:

@Expression("this.fallDistance > 0.0")
@Definition(id = "fallDistance", field = "Lnet/minecraft/entity/Entity;fallDistance:F")
@ModifyExpressionValue(method = "fall", at = @At("MIXINEXTRAS:EXPRESSION"))
private boolean yourHandler(boolean original) {  
    return original && MyMod.shouldFall(this);  
}

Let's unpack that:

  • The @At we use in the main @ModifyExpressionValue is @At("MIXINEXTRAS:EXPRESSION"). That simply means the actual target will be expressed in the @Expression annotation.
  • As you might expect from the name, @ModifyExpressionValue works great with expressions, and you can modify the result of any expression with it.
  • Our @Expression defines the code fragment we want to target. This is much more specific than a normal @At because it will not only check for the comparison, but also match the left and right hand sides.
  • We use @Definitions to define any identifiers used in our @Expression. In this example we define fallDistance as referring to the specific float fallDistance field in Entity. The formats accepted here are the same as those accepted in an @At("FIELD"). There are several built-in identifiers, like this, which you don't need to define.
  • The @Expression will (by default) return the "last" instruction in your expression. In this case that is the > comparison, because both the left and right must be evaluated before they can be compared.
  • Our handler method works like normal. We are modifying the result of a > comparison, so we take and return a boolean because that's what a comparison returns.
  • The @Expression language has no distinction between floats and doubles, so we simply use 0.0 for the right hand side.

Code Diff

- if (this.fallDistance > 0.0F) {
+ if (yourHandler(this.fallDistance > 0.0F)) {
...

Key things to note

  • @Expressions work with bytecode, not source code, like all of mixin. They are designed to be written in a way similar to source code, for your convenience, but they cannot magically match things you just copy and paste. This is also why you need @Definitions in the first place.
  • @Expressions can be used with any injector, with some special cases:
    • @ModifyExpressionValue can modify the result of any expression.
    • @WrapOperation can wrap all its normal things in addition to comparisons and array sets/gets
  • @Expressions cannot currently match expressions involving jumps, e.g. a && !b, a ? b : c, etc. The slight exception to this is comparisons, which can be matched, but can only be used as the outermost part of an expression. You could not for example match print(a == b). Of course you can use wildcards to match these things as part of a wider expression, see below for them...

Another example

Let's say we want to inject after this code:

this.emitGameEvent(GameEvent.ENTITY_MOUNT, passenger);

We could do this like so:

@Expression("this.emitGameEvent(ENTITY_MOUNT, ?)")  
@Definition(id = "emitGameEvent", method = "Lnet/minecraft/entity/Entity;emitGameEvent(Lnet/minecraft/world/event/GameEvent;Lnet/minecraft/entity/Entity;)V")
@Definition(id = "ENTITY_MOUNT", field = "Lnet/minecraft/world/event/GameEvent;ENTITY_MOUNT:Lnet/minecraft/world/event/GameEvent;")
@Inject(method = "addPassenger", at = @At(value = "MIXINEXTRAS:EXPRESSION", shift = At.Shift.AFTER))  
private void yourHandler(CallbackInfo ci) {  
    System.out.println("Hi!");  
}

Let's unpack the new stuff:

  • In our @Expression we used a wildcard: ?. It is not practical to define everything in a large expression, so we can use wildcards to omit the things we don't care about. Anything will match in their place. Note that wildcards can be used as expressions, like above, but also as identifiers, e.g. this.?(), which will match calls to any method on this that take no arguments.
  • We can use @Definitions to define both methods and fields. In each case the string should be of a format that the relevant type of @At accepts.
  • Static methods and fields are expressed with no receiver (i.e. SOME_FIELD, not SomeClass.SOME_FIELD)
  • We can use normal things in our @At("MIXINEXTRAS:EXPRESSION"), like shift and ordinal. Slices also work as expected. The @Expression itself will match the "last" thing in the chain, which is the emitGameEvent call, and the shift will go one instruction forward.
  • Code Diff

this.emitGameEvent(GameEvent.ENTITY_MOUNT, passenger);
+ yourHandler(new CallbackInfo());
...

A more complex example

Let's say we wanted to modify the result of this code:

new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState)

We could do this like so:

@Expression("new BlockStateParticleEffect(BLOCK, blockState)")  
@Definition(id = "BlockStateParticleEffect", type = BlockStateParticleEffect.class)  
@Definition(id = "BLOCK", field = "Lnet/minecraft/particle/ParticleTypes;BLOCK:Lnet/minecraft/particle/ParticleType;")  
@Definition(id = "blockState", local = @Local(type = BlockState.class))  
@ModifyExpressionValue(method = "spawnSprintingParticles", at = @At("MIXINEXTRAS:EXPRESSION"))  
private BlockStateParticleEffect yourHandler(BlockStateParticleEffect original) {  
    return YourMod.processParticle(original);  
}

Lots to unpack there:

  • @Definitions can contain not only methods and fields, but also types and locals. Types are to be used when targeting instantiations, instanceof checks, or casts. The @Local annotation works more or less as explained here, but with the addition of a type parameter to specify the type of the local. Note that because we do not specify an ordinal, this will only match if there is exactly 1 local of that type at the targeted place. In a real situation, I would probably use a wildcard for that local, since targeting locals is often brittle, but I've specified it here as an example.

Code Diff

- new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState)
+ yourHandler(new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState))
...

Quickfire Examples

Here are some pieces of Java code together with expressions you could use to match them:

  • this.pistonMovementDelta[i] = d;
    • We could use this.pistonMovementDelta[?] = ?
    • We could make @Definitions for the locals if we wanted, but that is likely to be more brittle here
  • nbt.putShort("Fire", (short)this.fireTicks);
    • We could use ?.putShort('Fire', (short) this.fireTicks)
    • Note that strings use single quotes since the entire expression will be in a string literal
    • However in actuality all we'd probably want is ?.putShort('Fire', ?)
  • entityKilled instanceof ServerPlayerEntity
    • We could use ? instanceof ServerPlayerEntity
    • Same comment as above about the local
  • return this.distance < d * d;
    • We could use return this.distance < ? * ?
    • Or we could specify the local

Targeted Expressions

I mentioned earlier that an @Expression will target the "last" thing in the expression, but this is only a default. Consider the code:

throw new IllegalStateException("Oh no!");

We might want to modify that exception before it is thrown, so to target it we could use the expression:

throw @(new IllegalStateException('Oh no!'))

The @(...) there is called a target. Instead of returning the "last" instruction (the throw), this will return the instantiation, allowing you to @ModifyExpressionValue it to your heart's content. If you do not target any expressions explicitly, then the entire expression is implicitly targeted, i.e.

this.myMethod(5)

is equivalent to

@(this.myMethod(5))

You can however have multiple explicit targets if you want.

Things to watch out for

  • Some things can't be distinguished in bytecode form:
    • true/false are equivalent to 1/0
    • Characters are just numbers, e.g. 'A' is equivalent to 65
    • Comparisons usually cannot be distinguished from their inverses. E.g. x >= y looks the same as x < y (but with an opposite effect). For that reason if you target a comparison make sure your expression is specific enough that it will only match what you expect. Note that due to NaN semantics, this gotcha does not apply to floats and doubles
    • Be particularly careful when using == 0 or != 0, because even the simple
       if (myBoolean) { ... }
      looks the same as
       if (myBoolean != false) { ... }
      and hence due to point 1, ? != 0 would match it, and due to point 3, ? == 0 would match it. To avoid this just make sure to use a specific expression instead of a wildcard.
  • It is not wise to target a wildcard. If you were to @ModifyExpressionValue it, you may not know what its concrete type is, i.e. it might be a subclass of what you were expecting, meaning your handler signature would be wrong. More generally, if the wildcard represents a "complex" expression (one involving jumps) then it would not be possible to target it at all, with any injector. Instead, try to use a different injector. E.g. say we wanted to modify the argument to:
     this.setX(...);
    I would not use this.setX(@(?)) with a @ModifyExpressionValue. Instead, use this.setX(?) with a @ModifyArg.

The @Expression language

Full documentation for the language.

Literals

'hello' // String
'x' // String or char
23 // int or long
0xFF // int or long
1.5 // float or double
true // boolean (or int, be careful)
null

Unary expressions

-x
~x
  • Note: a bitwise not is indistinguishable from x ^ -1, so it will match both

Binary expressions

a * b
a / b
a % b
a + b
a - b
a << b
a >> b
a >>> b
a & b
a ^ b
a | b
  • Precedences match java where applicable
  • & and | are bitwise, there is no way to match logical && or || (or !)

Comparisons

a == b
a != b
a < b
a <= b
a > b
a >= b
  • Comparisons must be top-level
  • You can @ModifyExpressionValue them and you can @WrapOperation them
  • For all types except doubles and floats, comparisons in bytecode cannot be distinguished from their inverted counterparts, so you must be careful that your expression is specific enough to avoid this issue

Identifier expressions

someLocal // load
someLocal = someValue // store
SOME_STATIC_FIELD // get
SOME_STATIC_FIELD = someValue // put
  • The identifier must be defined in a @Definition

Member expressions

x.someField // get
x.someField = someValue // put
  • The field's identifier must be defined in a @Definition
  • .length on arrays is built-in

Method calls

x.someMethod() // no arguments
x.someMethod(x) // 1 argument
x.someMethod(x, y) // 2 arguments
...
staticMethod() // no arguments
staticMethod(x) // 1 argument
  • The method's identifier must be defined in a @Definition

Wildcards

?
someObject.?
someObject.?(someArg)
  • Can either replace an entire expression or an identifier
  • Used for brevity or if it would be brittle to add a @Definition for the thing, e.g. with some locals
  • Can also match expressions which there is no way to explicitly specify, e.g. those involving jumps

Array operations

someArray[someIndex] // load
someArray[someIndex] = someValue // store
  • These have special support in @WrapOperation

Array creations

new SomeType[]{someValue, someOtherValue, aThirdValue} // filled array creation
new SomeType[someLength] // empty array creation
new SomeType[3][4][5] // multi-dimensional array creation
  • The type's identifier must be defined in a @Definition

Casts

(SomeType) someExpression
  • The type's identifier must be defined in a @Definition
  • Primitive casts are built-in, e.g. (float) x + y

Instanceof

x instanceof SomeType
  • The type's identifier must be defined in a @Definition

Instantiations

new SomeType() // no arguments
new SomeType(x, y) // 2 arguments
...
  • The type's identifier must be defined in a @Definition

Method references, constructor references and lambdas

::someMethod // unbound reference
someReceiver::someMethod // bound reference
SomeType::new // constructor reference

::someLambda // the lambda is considered unbound if it doesn't access `this`, i.e. its implementation is static
this::someLambda // if the lambda does access `this`, it is considered bound
  • Method references and lambdas are treated the same way, and must be appropriately defined in a @Definition
  • Lambdas that capture other local variables are treated as normal, it is only the capture of this that is important
  • Unbound references can match both non-static and static methods
  • Bound references obviously can only match non-static methods, since a receiver is required

Returns and throws

return someExpression
throw someException

Targets

@(someExpression)
// E.g.:
someMethod(@(new SomeType()))
this.something + @(this.somethingElse)
this.someMethod(@(value1), @(value2))
  • Each thing you target will be used by the injector
  • If you don't target anything explicitly then the entire expression is implicitly targeted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment