Eiffel callback trampolines

by Colin LeMahieu (modified: 2010 May 18)

Eiffel trampolines solve the problem of mismatched calling conventions between external languages like C/C++/ObjC and Eiffel.

The code for the library described is available at git://github.com/clemahieu/eiffel_trampolines.git

A useful reference for calling conventions is available at http://en.wikipedia.org/wiki/X86_calling_conventions

When doing a callback from C, we need to convert a function of the form func1 (int var1, double var2) in to a function of the form func2 (EIF_REFERENCE obj, int var1, double var2) and pass in an Eiffel object reference. One way to solve this is using a global variable that holds the Eiffel callback object. This approach has two drawbacks, it requires external C code and for every callback you wish to perform, you need a new global variable. This means you can only have a fixed number of callbacks as determined at compile time.

The Eiffel trampoline code at run-time creates a C compatible function along with an Eiffel object and Eiffel function to call back in to Eiffel. This means no external C code needs to be written, no global variables need to be used, and the number of callbacks is limited only by memory, and you can directly access structs passed in from C code.

From a C perspective, Eiffel uses an invisible first parameter, the `Current' object.

In Eiffel the feature

    callback (arguments: POINTER)

looks like

    callback (EIF_REFERENCE current, void * arguments)

in C.

We convert every C function, regardless of actual arguments, in to the form:

    callback (arguments: POINTER)

The position and contents of the arguments are platform and compiler specific and outlined below.

VCC/GCC x86 cdecl trampoline

The first diagram shows the cdecl x86 argument set up when performing a callback from C. The stack pointer, ESP, at time 0 points to the return address followed by each argument in order pushed to the stack. There is no padding between parameters. Cdecl pushes parameters right to left.

We thunk this call to Eiffel as shown in the second diagram. We push two parameters on to the stack pointer, the rightmost is ESP0 so we can reference the arguments passed in from the C side. The second is the Eiffel object we're operating on.

On the Eiffel side, ESP0 is the argument in to the callback feature. The first parameter can be accessed at offset 0x4. The second argument can be accessed at offset 0x4 + sizeof (arg1). The third argument can be accessed at offset 0x4 + sizeof (arg1) + sizeof (arg2) etc.

VCC x64 trampoline

The first diagram shows the VCC x64 argument set up from C. VCC on x64 passes the first 4 arguments in registers. Any argument less than 64bits still uses the entire register, the register is not zero extended. Any argument more than 64bits is passed by reference with a 64bit pointer. float and double parameters are passed in XMM registers. Any arguments beyond 4 are passed on the stack. Each argument on the stack is always 64 bits; arguments are padded with undefined contents if needed, structs bigger than 64 bits are passed by reference.

We thunk the call to Eiffel using a setup as outlined in the second diagram. All parameter registers are pushed to the stack and the first and second parameter register are filled with the target Eiffel object and the argument pointer to the stack. Notice the "Return address" and "Alignment padding" in the middle of the parameters. For simplicity and size concerns with the trampoline code, we have to work around the assembly level return address and 64 bits of alignment padding between the pushed register parameters and arguments above position 4. This means do not modify the contents of your argument pointer between [0x40, 0x50) or your program will blow up catastrophically.

If argument 1 is an integer type, it can be accessed at argument pointer offset 0x0. If argument 1 is a float/double type it can be accessed at argument pointer offset 0x20. Integer type/float type for a specific argument position is mutually exclusive, only one will be filled with meaningful information, no extra packing is done.

GCC x64 trampoline

The first diagram shows the GCC x64 argument set up from C. GCC puts the first 6 integer arguments and first 8 float/double arguments in registers. Any arguments less than 64 bits still use the entire register but are not zero-extended. Structs greater than 64 bits get packed in different ways depending on the struct. Some spill over in to the register for the next parameter, some pare passed on the stack. Unfortunately this may take some memory inspection to figure out where the parameters are being passed.

In the second diagram we outline the thunk to Eiffel. All the parameter registers are pushed to the stack. Notice the "Return address" in the middle of the parameter stack. This means you should not modify memory at argument pointer offset [0x70-0x78). We replace the first and second parameter registers with the target Eiffel object and the argument pointer to the stack.

GCC x64 Objective C trampoline

The first diagram shows the argument setup from Objective C. Objective C really translates down to C in the backend. Like Eiffel it has an invisible first parameter that is the object pointer. Objective C also has an invisible second parameter that is the "Selector", a C struct indicating which function is being called. Since the trampoline is targeted to a specific Eiffel feature, the Selector can be ignored.

The second diagram outlines how we thunk to Eiffel. The stack is similar to the GCC x64 C thunk except the first and second integer arguments are invisible from the Objective C side. The first Objective C argument is the third C-side argument. The labels for arguments in the second diagram represent the Objective C side argument number.

The Eiffel Object pointer is obtained differently in Objective C than in C. In Objective C when you replace an Objective C function, you're replacing that function for all instances of that class. It would hinder performance and increase complexity to pass all calls on all Objective C objects of a certain class, to a single Eiffel object instance. The way we handle this is by swapping the Objective C object pointer with an Eiffel object pointer, obtained from an instance variable of the Objective C class. In trampoline construction you pass in an Objective C Ivar for that class, which is the Ivar the trampoline will use to retrieve the Eiffel object pointer for a given Objective C object.

Usage example

class APPLICATION inherit ARGUMENTS create make feature {NONE} -- Initialization make local tramp: TRAMPOLINE b: B heap: HEAP do create heap.make ({NATURAL_32}1024 * {NATURAL_32}1024) create b tramp := b.tramp6 (heap) callout5 (tramp.function) end callout5 (function: POINTER) external "C inline" alias "[ void (*typed_func) (int, int, double, double, double, double); typed_func = $function; typed_func (4, 3, 7.8, 8.777779999, 2.345, 1.234); ]" end end class B feature tramp6 (heap: HEAP): TRAMPOLINE local obj: POINTER func: POINTER do obj := curr (Current) func := $b6 create Result.make (heap, obj, func) end curr (in: ANY): POINTER external "C inline" alias "[ return eif_adopt ($in); ]" end i: INTEGER b6 (arguments: POINTER) local pointer: MANAGED_POINTER one: INTEGER_32 two: INTEGER_32 three: REAL_64 four: REAL_64 five: REAL_64 six: REAL_64 do create pointer.share_from_pointer (arguments, 44) one := pointer.read_integer_32 (4) two := pointer.read_integer_32 (8) three := pointer.read_real_64 (12) four := pointer.read_real_64 (20) five := pointer.read_real_64 (28) six := pointer.read_real_64 (36) i := i + five.truncated_to_integer + six.truncated_to_integer end end