Pharo FFI on aarch64/arm64

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
3 messages Options
Reply | Threaded
Open this post in threaded view
|

Pharo FFI on aarch64/arm64

ken.dickey
 
Greetings,

I have been making small progress in getting Pharo's FFI unit tests to
pass on arm64.

Built from recent
   OpenSmalltalk opensmalltalk-vm build.linux64ARMv8/pharo.stack.spur

I get a pharo vm whose UI is pretty slow (no JIT so far), so current
progress is slow.

I am looking at #FFICallbackThunk>initializeARM32 but I don't yet
understand quite what this code is trying to do, so have not made proper
changes for arm64.

Anybody care to join in or advise me on this?

Note that I am neither a Pharo nor a UnifiedFFI user, so I may be making
really dumb mistakes at this point (i.e. _you_ could help me out here!  
8^).

Thanks much in advance,
-KenD


ARM64FFI.tgz (4K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Pharo FFI on aarch64/arm64

Eliot Miranda-2
 
Hi Ken,

On Thu, Mar 14, 2019 at 2:09 PM <[hidden email]> wrote:
 Greetings,

I have been making small progress in getting Pharo's FFI unit tests to
pass on arm64.

Built from recent
   OpenSmalltalk opensmalltalk-vm build.linux64ARMv8/pharo.stack.spur

I get a pharo vm whose UI is pretty slow (no JIT so far), so current
progress is slow.

I am looking at #FFICallbackThunk>initializeARM32 but I don't yet
understand quite what this code is trying to do, so have not made proper
changes for arm64.

Anybody care to join in or advise me on this?

Note that I am neither a Pharo nor a UnifiedFFI user, so I may be making
really dumb mistakes at this point (i.e. _you_ could help me out here! 
8^).

Can we start from the last section of the comment for which I wrote on February 6th?  I wrote the following.  What is missing from this information that you need to understand how to proceed?

Callbacks are not handled by this plugin.  Instead they are handled by image-level code to create Alien "thunks", small sequences of machine code, that invoke a function thunkEntry, implemented as required by each platform in one of the files in platforms/Cross/plugins/IA32ABI (a regrettable name; it should be something like platforms/Cross/plugins/AlienCallbacks).  Each platform's thunkEntry has a signature that takes all integer register parameters (if any), all floating-point register parameters (if any), a pointer to the thunk, and the stack pointer at the point the thunk was invoiked.  To pass a callback to foreign code, the marshaller in this plugin passes the address of the thunk.  When external code calls that address the think is invoked, and it invokes thunkEntry as required.  thunkEntry then saves its parameters locally in an instance of VMCallbackContext, which includes a jmpbuf.  thunkEntry then does a setjmp and enters the VM via sendInvokeCallbackContext[:], which creates an activation of invokeCallbackContext: to execute the callback.  This method then extracts the address of the thunk from the VMCallbackContext and invokes machinery in Callback to match the address with a Smalltalk block.  The block is then evaluated with the parameters extracted from the VMCallbackContext, marshalled by methods in Callback subclasses signatures protocol, which know whether a parameter would be passed in a register or on the stack.  The result of the block is then passed back to thunkEntry by storing it in a field in the VMCallbackContext and invoking primSignal:andReturnAs:fromContext:, which uses longjmp to jump back to thunkEntry which then extracts the result from its VMCallbackContext and returns.

For  example, the signature of thunkEntry on x86/IA32 (platforms/Cross/plugins/IA32ABI/ia32abicc.c) is
long
thunkEntry(void *thunkp, sqIntptr_t *stackp)
whereas on ARMv4/5/6 (platforms/Cross/plugins/IA32ABI/arm32abicc.c) it is
long long
thunkEntry(long r0, long r1, long r2, long r3,
           double d0, double d1, double d2, double d3,
           double d4, double d5, double d6, double d7,
           void *thunkpPlus16, sqIntptr_t *stackp)
 
Thanks much in advance,
-KenD

_,,,^..^,,,_
best, Eliot
Reply | Threaded
Open this post in threaded view
|

Re: Pharo FFI on aarch64/arm64

Eliot Miranda-2
In reply to this post by ken.dickey
 
Hi Ken,

On Thu, Mar 14, 2019 at 2:09 PM <[hidden email]> wrote:
 Greetings,

I have been making small progress in getting Pharo's FFI unit tests to
pass on arm64.

Built from recent
   OpenSmalltalk opensmalltalk-vm build.linux64ARMv8/pharo.stack.spur

I get a pharo vm whose UI is pretty slow (no JIT so far), so current
progress is slow.

I am looking at #FFICallbackThunk>initializeARM32 but I don't yet
understand quite what this code is trying to do, so have not made proper
changes for arm64.

Anybody care to join in or advise me on this?

Note that I am neither a Pharo nor a UnifiedFFI user, so I may be making
really dumb mistakes at this point (i.e. _you_ could help me out here! 
8^).

Oops!  I should have directed your attention to the comment for Callback that I wrote back in September of 2016:

Callbacks encapsulate callbacks from the outside world.  They allow Smalltalk blocks to be evaluated and answer their results to external (e.g. C) callees.  Callbacks are created with signature:block:, e.g.

cb := Callback
signature:  #(int (*)(const void *, const void *))
block: [ :arg1 :arg2 | ((arg1 doubleAt: 1) - (arg2 doubleAt: 1)) sign].

and passed through the FFI by passing their pointer, e.g.

self qui: data ck: data size so: 8 rt: cb pointer

When the callback is made, the system arranges that the block is invoked with the arguments as defined by the signature, and the result of the block passed back, again as defined by the signature.  See methods in the signatures protocol in subclasses of Callback for signature methods that decode the C stack and registers to invoke a callback with parsed arguments.  See Callback>>valueInContext: and subclass implementations for the evaluation of the signature method that invokes the callback block with correctly parsed arguments.  See Alien class>>invokeCallbackContext: for the entry-point for callbacks into the system from the VM.

Instance Variables:
block <BlockClosure> - The Smalltalk code to be run in response to external code invoking the callback.
thunk <FFICallbackThunk> - the wrapper around the machine-code thunk that initiates the callback and whose address should be passed to C
evaluator <Symbol> - the selector of the marshalling method to use; see methods in the signatures protocol in subclasses of Callback.
numEvaluatorArgs <Integer> - the arity of evaluator
argsProxyClass <Alien subclass> - legacy; unused; the wrapper around the thunk's incoming stack pointer, used to extract arguments from the stack.

Class Variables:
ThunkToCallbackMap <Dictionary of: thunkAddress <Integer> -> callback <Callback>> - used to lookup the Callback associated with a specific thunk address on callback.  See FFICallbackThunk.
ABI <String> - the name of the current ABI

Class Instance Variables
concreteClass <Callback subclass> - the concrete class for callbacks on the current platform, or nil if one doesn't yet exist.

Implementation:
The way that it works is in two parts
- on callback the VM passes up a pointer to a structure from which all arguments, stacked and in registers (because the VM has copied any register args into the struct) can be accessed, and through which the result can be returned.
- the image level provides marshalling methods that match the signature in the callback.  Marshalling methods belong in concrete subclasses, one subclass for each ABI.

So e.g. with a callback of
Callback
signature:  #(int (*)(const void *, const void *))
block: [ :arg1 :arg2 | ((arg1 doubleAt: 1) - (arg2 doubleAt: 1)) sign]
the marshalling methods are in one of Callback's concrete subclasses signatures protocol, for example

CallbackForIA32>>voidstarvoidstarRetint: callbackContext sp: spAlien
<signature: #(int (*)(const void *, const void *))>
^callbackContext wordResult:
(block
value: (Alien forPointer: (spAlien unsignedLongAt: 1))
value: (Alien forPointer: (spAlien unsignedLongAt: 5)))

where spAlien is an Alien pointing to a VMCallbackContext32.

For ARM support, where there the first four integer arguments are passed in registers, we can use

CallbackForARM32>>voidstarvoidstarRetint: callbackContext regs: regsAlien
<signature: #(int (*)(const void *, const void *))>
^callbackContext wordResult:
(block
value: (Alien forPointer: (regsAlien unsignedLongAt: 1))
value: (Alien forPointer: (regsAlien unsignedLongAt: 5)))

The selector of the method doesn't matter, providing it doesn't conflict with any other, except for the number of arguments.  What's important is the pragma which defines the signature and the ABI for which this is a valid marshalling method.  Support for callee pop callbacks (Pascal calling convention such as the Win32 stdcall: convention) are supported using the <calleepops: N> pragma which specifies how many bytes to pop.

When a callback is instantiated, Callback introspects to find the marshalling method that matches the signature for the current ABI.  If one doesn't already exist you can write one.  Hopefully we'll write an ABI compiler that will automatically generate these marshalling methods according to the platform's ABI, but for now its a manual process.; at least it's open and flexible.  When the callback is invoked the evaluator is performed with the current callbackContext and pointer(s) to the arguments.  There is a 32-bit and a 64-bit callback context, and it can have a stack pointer, integer register args and floating point register args, so it's general enough for any callback.

To pass back the result, a value is assigned into the struct via the accessor in the marshalling method and control returns to teh point where teh callback comes in, and this uses a primitive to return.  Inside the callbackContext is a jmpbuf from a setjmp.  The primitive longjmp's back to the entry point in the VM which extracts the result and the code for the kind of result and returns.  See Callback class>>invokeCallbackContext:
 
Thanks much in advance,
-KenD

_,,,^..^,,,_
best, Eliot