ffmasm is an assembler for hand-assembling from Java.
It uses Foreign Function & Memory API, so the application can call assembled code via MethodHandle.
- Javadoc: https://yasuenag.github.io/ffmasm/
- Maven package: https://github.com/YaSuenag/ffmasm/packages/
- Supported instructions: See builder classes in com.yasuenag.ffmasm.amd64.
Java 22
- Linux AMD64
- Windows AMD64
$ mvn package
$ mvn test
If you want to run tests for AVX, set true
to avxtest
system property.
$ mvn -Davxtest=true test
See Javadoc and cpumodel examples.
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()){
...
}
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.critical(false));
NOTE: Linker.Option.critical() 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 critical()
.
int ret = (int)method.invoke(100); // "ret" should be 100
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);
}
You can record both function name and entry point address as a perf map file.
You can pass function name into build()
method:
.build("GeneratedFunc", Linker.Option.critical(true));
Function name would be set to <unnamed>
if you do not pass function name (includes calling build(Linker.Option)
).
perf map file would be written at shutdown hook when CodeSegment
lives. All of functions in all of CodeSegment
s which are lives would be dumped at the time of shutdown hook.
You need to enable perf map dumper via CodeSegment::enablePerfMapDumper
. Call CodeSegment::disablePerfMapDumper
if you want to cancel the dumper.
perf tool on Linux supports JIT-generated code. ffmasm can dump generated code as a jitdump. See an example for details.
Pass JitDump
insntace to build
method.
jitdump = JitDump.getInstance(Path.of("."));
:
.build("GeneratedFunc", jitdump);
Then you can run perf record
. Note that you have to set monotonic clock with -k
option.
perf record -k 1 $JAVA_HOME/bin/java ...
As a result, you would get jit-<PID>.dump
which includes JIT information. You should keep until run perf inject
.
perf.data
generated by perf record
would not include JIT'ed code, so you need to inject them via perf inject
as following.
perf inject --jit -i perf.data -o perf.jit.data
You will get some .so
file and perf.jit.data
as an injected file as a result.
perf report -i perf.jit.data
The GNU Lesser General Public License, version 3.0