YaSuenag / ffmasm

hand-assembler for Java

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ffmasm

CI result CodeQL

ffmasm is an assembler for hand-assembling from Java.
It uses Foreign Function & Memory API, so the application can call assembled code via MethodHandle.

Requirements

Java 21

Supported platform

  • Linux AMD64
  • Windows AMD64

How to build

$ mvn package

Test for ffmasm

$ mvn test

If you want to run tests for AVX, set true to avxtest system property.

$ mvn -Davxtest=true test

How to use

See Javadoc and cpumodel examples.

1. Create CodeSegment

CodeSegment is a storage for assembled code. In Linux, it would be allocated by mmap(2) with executable bit.
It implements AutoCloseable, so you can use try-with-resources in below:

try(var seg = new CodeSegment()){
  ...
}

2. Create MethodHandle via AMD64AsmBuilder

You can assemble the code via AMD64AsmBuilder. It would be instanciated via create(), and it should be passed both CodeSegment and FunctionDescriptor.

In following example, the method is defined as (I)I (JNI signature) in FunctionDescriptor.
AMD64AsmBuilder is builder pattern, so you can add instruction in below. Following example shows method argument (int) would be returned straightly.

You can get MethodHandle in result of build().

var desc = FunctionDescriptor.of(
             ValueLayout.JAVA_INT, // return value
             ValueLayout.JAVA_INT // 1st argument
           );

var method = AMD64AsmBuilder.create(seg, desc)
    /* push %rbp         */ .push(Register.RBP)
    /* mov %rsp, %rbp    */ .movRM(Register.RSP, Register.RBP, OptionalInt.empty())
    /* mov %rdi, %rax    */ .movRM(Register.RDI, Register.RAX, OptionalInt.empty())
    /* leave             */ .leave()
    /* ret               */ .ret()
                            .build(Linker.Option.isTrivial());

NOTE: Linker.Option.isTrivial() is recommended to pass build() method due to performance, but it might be cause of some issues in JVM (time to synchronize safepoint, memory corruption, etc). See Javadoc of isTrivial().

3. Method call

int ret = (int)method.invoke(100); // "ret" should be 100

Play with JNI

You can bind native method to MemorySegment of ffmasm code dynamically.

You have to construct MemorySegment of the machine code with AMD64AsmBuilder, and you have to get it from getMemorySegment(). Then you can bind it via NativeRegister.

Following example shows native method test is binded to the code made by ffmasm. Note that 1st argument in Java is located at arg3 in native function because this is native function (1st arg is JNIEnv*, and 2nd arg is jobject or jclass).

public native int test(int arg);

<snip>

try(var seg = new CodeSegment()){
  var desc = FunctionDescriptor.of(
               ValueLayout.JAVA_INT, // return value
               ValueLayout.JAVA_INT, // 1st arg (JNIEnv *)
               ValueLayout.JAVA_INT, // 2nd arg (jobject)
               ValueLayout.JAVA_INT  // 3rd arg (arg1 of caller)
             );
  var stub = AMD64AsmBuilder.create(AMD64AsmBuilder.class, seg, desc)
    /* push %rbp         */ .push(Register.RBP)
    /* mov %rsp, %rbp    */ .movRM(Register.RBP, Register.RSP, OptionalInt.empty())
    /* mov %arg3, retReg */ .movMR(argReg.arg3(), argReg.returnReg(), OptionalInt.empty()) // arg1 in Java is arg3 in native
    /* leave             */ .leave()
    /* ret               */ .ret()
                            .getMemorySegment();

  var method = this.getClass()
                   .getMethod("test", int.class);

  var methodMap = Map.of(method, stub);
  var register = NativeRegister.create(this.getClass());
  register.registerNatives(methodMap);

  final int expected = 100;
  int actual = test(expected);
  Assertions.assertEquals(expected, actual);
}

Memory pinning

You can pin Java primitive array via Pinning. It is same semantics of GetPrimitiveArrayCritical() / ReleasePrimitiveArrayCritical() in JNI. It expects performance improvement when you refer Java array in ffmasm code. However it might be serious problem (e.g. preventing GC) if you keep pinned memory long time. So you should use it carefully.

Following example shows pin array at first, then we modify value via MemorySegment.

int[] array = new int[]{1, 2, 3, 4};

var pinnedMem = Pinning.getInstance()
                       .pin(array)
                       .reinterpret(ValueLayout.JAVA_INT.byteSize() * array.length);
for(int idx = 0; idx < expected.length; idx++){
  pinnedMem.setAtIndex(ValueLayout.JAVA_INT, idx, idx * 2);
}
Pinning.getInstance().unpin(pinnedMem);

// array is {0, 2, 4, 6} at this point

License

The GNU Lesser General Public License, version 3.0

About

hand-assembler for Java

License:GNU Lesser General Public License v3.0


Languages

Language:Java 99.0%Language:Shell 0.5%Language:Assembly 0.3%Language:Makefile 0.2%