abi-cafe 🧩☕️❤️

Not sure if your compilers have matching ABIs? Then put them through the ultimate compatibility crucible and pair them up on a shift at The ABI Café! Find out if your one true pairing fastcalls for each other or are just another slowburn disaster. (Maid outfits optional but recommended.)

About

Run --help to get options for configuring execution.

This tool helps automate testing that two languages/compilers agree on ABIs for the purposes of FFI. This is still in early development so lots of stuff is stubbed out.

The principle of the tool is as follows:

By running this natively on whatever platform you care about, this will tell you what FFI interfaces do and don't currently work. Ideally all you need to do is cargo run, but we're dealing with native toolchains so, expect toolchain bugs!

By default we will:

But you can the CLI interface lets you override these defaults. This is especially useful for --pairs because it lets you access more specific pairings, like if you really want to specifically test gcccallsclang.

Supported Features

Here are the current things that work.

Implementations

"ABI Implementations" refer to a specific compiler or language which claims to implement some ABIs. The currently supported AbiImpls are:

By default, we test the following pairings:

In theory other implementations aren't too bad to add. You just need to:

See the Test Harness section below for details on how to use it.

Calling Conventions

Each language may claim to support a particular set of calling conventions (and may use knowledge of the target platform to adjust their decisions). We try to generate and test all supported conventions.

Universal Conventions:

Windows Conventions:

Any test which specifies the "All" will implicitly combinatorically generate every known convention. "Nonsensical" situations like stdcall on linux are the responsibility of the AbiImpls to identify and disable.

Types

The test format support for the following types/concepts:

Adding Tests

Tests are specified as ron files in the test/ directory, because it's more compact than JSON, has comments, and is more reliable with large integers. Because C is in some sense the "lingua franca" of FFI that everyone has to deal with, we prefer using C types in these definitions.

You don't need to register the test anywhere, we will just try to parse every file in that directory.

The "default" workflow is to handwrite a ron file, and the testing framework will handle generating the actual code implementating that interface (example: structs.ron). Generated impls will be output to the generated_impls dir for debugging. Build artifacts will get dumped in target/temp/ if you want to debug those too.

Example:

rust Test( // name of this set of tests name: "examples", // generate tests for the following function signatures funcs: [ ( // base function/subtest name (but also subtest name) name: "some_prims", // what calling conventions to generate this test for // (All = do them all, a good default) conventions: [All], // args inputs: [ Int(c_int32_t(5)), Int(c_uint64_t(0x123_abc)), ] // return value output: Some(Bool(true)), ), ( name: "some_structs", conventions: [All], inputs: [ Int(c_int32_t(5)), // Struct decls are implicit in usage. // All structs with the same name must match! Struct("MyStruct", [ Int(c_uint8_t(0xf1)), Float(c_double(1234.23)), ]), Struct("MyStruct", [ Int(c_uint8_t(0x1)), Float(c_double(0.23)), ]), ], // no return (void) output: None, ), ] )

However, you have two "power user" options available:

The Test Harness

Implementation details of dylib test harness are split up between main.rs and the contents of the top-level harness/ directory. The contents of harness/ include:

Ideally you shouldn't have to worry about how the callbacks work, so I'll just focus on the idea/usage. To begin with, here is an example of using this interface:

```C // Caller Side uint64t basicval(struct MyStruct arg0, int32_t arg1);

// The test harness will invoke your test through this symbol! void dotest(void) { // Initialize and report the inputs struct MyStruct arg0 = { 241, 1234.23 }; WRITE(CALLERINPUTS, (char)&arg0.field0, (uint32_t)sizeof(arg0.field0)); WRITE(CALLER_INPUTS, (char)&arg0.field1, (uint32t)sizeof(arg0.field1)); FINISHEDVAL(CALLER_INPUTS);

int32_t arg1 = 5;
WRITE(CALLER_INPUTS, (char*)&arg1, (uint32_t)sizeof(arg1));
FINISHED_VAL(CALLER_INPUTS);

// Do the call
uint64_t output = basic_val(arg0, arg1);

// Report the output
WRITE(CALLER_OUTPUTS, (char*)&output, (uint32_t)sizeof(output));
FINISHED_VAL(CALLER_OUTPUTS);

// Declare that the test is complete on our side
FINISHED_FUNC(CALLER_INPUTS, CALLER_OUTPUTS);

} ```

```C // Callee Side uint64t basicval(struct MyStruct arg0, int32t arg1) { // Report the inputs WRITE(CALLEEINPUTS, (char)&arg0.field0, (uint32_t)sizeof(arg0.field0)); WRITE(CALLEE_INPUTS, (char)&arg0.field1, (uint32t)sizeof(arg0.field1)); FINISHEDVAL(CALLEE_INPUTS);

WRITE(CALLEE_INPUTS, (char*)&arg1, (uint32_t)sizeof(arg1));
FINISHED_VAL(CALLEE_INPUTS);

// Initialize and report the output
uint64_t output = 17;
WRITE(CALLEE_OUTPUTS, (char*)&output, (uint32_t)sizeof(output));
FINISHED_VAL(CALLEE_OUTPUTS);

// Declare that the test is complete on our side
FINISHED_FUNC(CALLEE_INPUTS, CALLEE_OUTPUTS);

// Actually return
return output;

} ```

The high level idea is that each side:

There are 4 buffers: CALLERINPUTS, CALLEROUTPUTS, CALLEEINPUTS, CALLEEOUTPUTS. Each side should only use its two buffers.

The signatures of the callbacks are:

Doing things in this very explicit way gives the test harness a better semantic understanding of what the implementations think is happening. This helps us emit better diagnostics and avoid cascading failures between subtests.

Trophy Case