Hello, everyone, and welcome to my talk about the foreign function interface, also known
as Project Panama.
I'm modern-der, and I've implemented the foreign function interface for the power architecture,
also known as PowerPC.
Obviously, I'm in the SAP machine team.
SAP machine is our open-tradicated distribution.
But we don't only make distributions.
We are also one of the biggest contributors to open-tradicated.
It's already mentioned by Mark this morning.
So it's good to have you here, and let's immediately get started
with an example.
So let's assume we have native library.
It's called Time.SO, and it contains function get time with some signature.
It takes C string in a length, and now the task is to call this function from Java.
So of course, we can traditionally do this with train I.
So by writing a C layer, and then we can call it from Java, but let's skip that,
and jump directly to the foreign function interface.
So we can now do everything directly from Java.
There's no need for an extra C layer anymore.
And by the way, the term down call means we're calling from Java to native.
So this direction.
So what does this example code do?
In the static initializer, we load the time.SO, the library.
And then we need to set up a couple of other things before we can use it.
So first of all, we describe the array with the size and the type.
So in Java, the Java array is represented by a Java byte array.
And we also need to describe the signature of the get time function.
Then we will need the address of the function.
We use a simple look up for that that allows us to create a memory segment for that.
So the address is represented as a memory segment.
Then we can use the native linker to create a down call handle.
I'll explain that in a minute.
So once we have set all of this up, it's pretty easy to use.
We can just use a flat local error arena.
And then create or allocate the array.
And then pass it to the function.
The get time method handle invokes the free or the time function in the native library.
And at the end, we only need to convert the results, which we're written into the array
back to a Java string, which we can print out.
So I promise to tell more about the native linker, what does native linker do?
It finds us a linker for the platform we are using.
There are 10 different linkers implemented within the tradi-k for 10 64 bit platforms.
On X86, we have one linker, which is used for Linux and Mac.
On ARM, 64, there are in total three linkers, different calling conventions.
And also in power, which I have worked on, we have three linkers.
Power processors can run in both little and big-end-in mode.
And modern Linux uses little-end-in, which comes with API for version 2.
So it's a newer calling convention.
We obviously look versions still use peak engine and the old API.
That platform is already out of support, actually, but we're still keeping it alive,
because the API is very similar to AIX.
And as you can see, AIX is still supported.
It's IBM's traditional Unix.
And in addition to that, we also support risk-5 and S490 on Linux.
So quite a few native linkers were implemented, but what about other platforms?
Can we run it on a 32 bit machine, for example?
Yes, we can.
Other platforms are supported by a lip-F-FI.
But that means we need a separate library, and we have to call for it,
and it makes it, of course, a bit slower.
So once we have the native linker, we can use it to create a down call handle.
And this is my most complicated slide.
There's a lot of things to do for that, besides some tracks and caching,
downhill handle, classifies the arguments and the return types.
For example, structures can be passed in registers, or by reference,
and some platforms have special rules for how much in-spload
aggregates, a word is terrible to pronounce,
and it was even worse to implement.
It's only on some platforms, and then there are more simple types
like pointer, pointer, integer, and float.
Once we have classified them, the down call handle can create bindings.
Bindings are a recipe for processing the arguments
and the return types, and they are implemented by operators.
It's modeled as a stack-based interpreter,
a bit similar to a Java byte code.
And I have some examples on this slide.
For example, the top operator, it just duplicates the top of stack.
And then there's a buffer load that loads a value from a memory statement.
And we have the M-store that writes a value into a register,
or into a stack slot.
There are much more, but I can't go into all details.
It's not only similar to Java byte code.
It also gets compiled or translated to Java byte code.
There's a binding specializer, and that generates,
as I said, Java byte code from the bindings.
It can be switched off by a property, by default, it's enabled.
It can also be dumped, so you can see what byte code
has got generated.
And it's a bit slow when you have the first invocation.
But after time, if you call it bare frequently,
the generated byte code will also get just in time compiled.
And then after some time, it will be fast.
But that's not all.
We still need the backend.
The backend is called via train I.
There's a make-downhold stop.
And by the way, we're still using train I.
So it's still needed, at least inside the Trader M or the Trader K.
Yeah, we'd need it at least to generate code
for the foreign function in the face.
So next slide, we can trace what the backend does.
At least if we have a debug build,
we can use this log flag.
And if we have an HsDIS library, also in place,
this allows us to see a disassembled code.
So the backend generates binary code into the code cache.
And with the logging, we can see that disassembled.
It's a large enough to read, I think.
Pardon?
What does this code do?
At the beginning, I also, this is from X86.
In this case, the first instruction
created a stack frame.
Then we have a flat-stage transition to native.
Before this point, garbage collector
perhaps need to synchronize this hour flat.
Because obviously, Java flat can access or modify
Java objects and garbage collector can move things around.
So that needs to be coordinated.
And after this third-stage transition,
the garbage collector flat, let's know
that we are in native, in a native function.
And we will no longer access Java objects,
so that they can do what they want with a Java heap.
So next step is to set up the argument.
So in this case, all arguments are passed in registers.
We don't need any stacks slots.
And then here, this is the call to the native function.
And once that is done, we need to change the steps
back to in Java.
And this direction is a bit more complicated.
It includes a safe point check and also memory barrier.
Yeah, the memory barrier may be a performance bottleneck,
especially if you call this function very often.
And you have a large system with many sockets,
or at least many chips, because the memory barrier
requires internship communication
and that may increase the latency.
There are several ways to get rid of the memory barrier.
So by the way, it's a lock-at instruction on X86.
And we can use the system memory barrier instead.
So hopefully not me.
Yeah, the system memory barrier makes this code cheaper,
because the lock-at instruction will be omitted.
But on the other side, it makes other things slower, for example,
especially hand shakes will be slower with that.
So it's not always beneficial.
But it is possible to get rid of all that state transitions.
Some of you may know critical train ironatives
from all the telecommunication releases.
And that is now also possible with a foreign function
interface.
You can use it by a linker option.
So this looks like this.
We just need to add the linker option
to the down call handle, linker option critical,
and by the Boolean argument here,
we specify that the function is allowed to access the Java
heap directly, and that makes things even more simple.
So after that, we can just use a normal byte array.
And we can pass that as a memory segment,
and finally, we can route the array to a Java string,
which we can print, so it's pretty simple now.
There's a warning on this slide.
Do not use for anything which may block.
The type state transitions were there for a reason.
If you now have anything which blocks,
then you will also block the tree sees what we have
to see developers in the room here,
and they will not be happy if you block that.
So don't do this.
Maybe it would be an idea for April 1st and file a machine.
All right, so what happens if we do this with the back end?
You can see the generated code is pretty simple now.
There's nothing's low in it anymore.
You can also see the address computation is now there.
We compute the start of the array address,
and the native function will directly write to that.
So there are more linker options.
So the next one is on this slide.
It is possible to figure out the kernel,
and the linker option is called capture call state.
If we use that, we need to pass an additional memory segment
for the captured state.
And I'm using here a bar handle
to extract the kernel field from the structure after the call.
So this is also a good feature that we can figure out
the kernel after native function call.
But that adds some code to the generated code,
which are directly after the native function call,
we see another function call to capture state,
so that adds to some overhead.
So my example was a bit simple.
It only used an array, but we can also pass more complicated
structures like an IDF.
So IDF has nothing to do with Israeli defense forces.
It stands for in double float.
And some platform structure lay out rules require padding,
which you can see here and here.
You probably would not write this manually.
There is a tool to generate such code.
It's called tray extract.
Trace extract is based on LLVM.
And it parses, see how to file and it generates Java code.
So it's this code now platform independent.
Not always, you can also see it in the comment here.
A X, for example, has a bit different layout rules.
It doesn't use padding here by default.
But fortunately, there's a hack.
We can build the native library on AX with Pratma,
a line-atro.
And that will make the layout compatible with Linux.
If we don't do this, we have to modify the Java code
for AX.
It's a bit unfortunate.
So I have talked 16 minutes about the on-calls.
And now I have to get to upcalls.
But I will make the short.
What is the upcall?
Upcall is the other direction.
It is called from native to Java.
And the upcall stop provides us a memory segment,
which can be passed to a native function,
and serve as a function pointer for callback.
So the native function can callback to Java, which is pretty nice.
It generates a bit similar code to the downcall handle,
which I'm not showing here.
But it's more complex.
It needs to save more registers.
And it requires additional c-calls for resource management.
So upcalls are always lower than downcalls.
So let's sum it up.
There's several ways to call native function.
The traditional way is train I.
And we have critical functions available.
We enable it by the switch, critical train I.
native.
But it's only available until the tridicase 17.
It was removed later.
And with a new function in the phase,
we have also critical functions available since tridicate
22.
As the link option, as I already showed.
So what can we do for tridicate 21?
There's one more option.
It's project Nalim.
And the author is also here in the room.
Yeah, it is only for critical function.
It is based on tridicate, MCI,
on the Java virtual machine compiler interface.
But it's not supported on all platforms.
And it's not supported by the Open Tridicase.
So we use it at your own risk.
So I think I have a bit more time to talk about
the power-specific things.
I have mentioned that I have plotted it to power.
I have one extra slide for that.
So power has some special properties.
The ABI has some special properties,
which were not implemented before.
For example, integers need to be passed as long,
so we need sign extent floating points of loads.
Need an instruction to convert it to double format,
because floating of load values are represented
as double format when they are passed in registers.
Then the HFA is extremely complicated.
The homogenous float aggregate, some parts are typically passed
in floating point registers.
And once we have all floating point registers used,
the rest gets passed in general purpose registers.
And if they are not enough free channel purpose registers,
we also use the exploits.
And the float registers used double format always.
But the other parts may be compressed
with 32 bits.
And there are cases in which we need to use both.
Retsrester and FlexStex.for the same value.
So that was terrible to implement.
As I mentioned, we have three different ABI's and power.
Big engine needed some extra code for big engine.
We had to shift the value into the values into the right position.
And they had to extend the binding operators
and all that, so that was some extra work to be done.
So I'm already done now.
Panama can be used free of charge.
It's open source.
I'm sure some politicians will like the statement.
Don't forget to grab a submachine sticker.
And I think it's time for questions now.
APPLAUSE
Yeah?
Those native blocks can be combined into the color as well.
The question is if these native blocks can inline,
no, they can't only the parts which are in Java.
So the general is byte code.
They can be inline, but not the blocks.
Any more questions?
So I think we're done.
And if any more questions come into your mind,
they can find me here around.
I'll be here for the rest of the day.
So the next talk will also be about the foreign function
in the face of stay tuned.
APPLAUSE