ABI: The Contract of Argument Passing Between Different Runtimes

Md. Ahsanul Haque

29 June, 2026

The Hidden Binary Treaty That Makes Languages Understand Each Other

When you call a function in another language –
Dart → C,
Java → C++,
Swift → Objective-C,
Rust → Go,
Python → C modules 

A mysterious, invisible contract determines whether the call succeeds or the entire process explodes.

That contract is the ABI: Application Binary Interface.

An ABI is the machine-level diplomatic treaty that allows different runtimes, languages, compilers, and OSes to talk to each other safely. Without it, there is no FFI, no plugins, no Python C extensions, no Node.js N-API, no JVM native methods, no Swift bridging, no Go interop, no Rust FFI, no Flutter engine.

This blog explains the ABI in general, for all languages and all runtimes, with specific examples from Android/iOS (ARM64) to make the abstract concepts concrete.

1. What Exactly Is an ABI? (General Definition)

An ABI is not a language feature, not C, not Java, not Swift.
An ABI is a machine-level specification for:

✔ Call convention

How arguments are passed: which registers, which stack slots, in what order.

✔ Return convention

Which register returns values, how structs are returned.

✔ Stack frame layout

How a function pushes/pops stack frames, aligns them, and stores local variables.

✔ Register saving rules

Which registers the caller must save.This prevents functions from clobbering each other’s data.

✔ Struct / Class / Enum layout

Memory alignment, padding, packing rules.

✔ Dynamic linking rules

Symbol naming
Library loading rules
Relocation mechanism

✔ Exception propagation rules

C++ exceptions, Swift exceptions, JVM exceptions – all must obey ABI-defined stack unwinding.

2. Why All Runtimes Must Obey ABI

Because CPU doesn’t speak: 

  •    Dart
  •    Swift
  •    Java
  •    Python
  •    Go 
  •    Kotlin
  •    PHP
  •    Node.js
  •    Rust
  •    C++

The CPU speaks only opcodes, registers, stack, pointers, and memory addresses.

Thus every runtime must obey the OS + CPU ABI, or the machine cannot run the code correctly.

3. Different OS + CPU = Different ABI

  OS

    CPU

ABI Example

Android

  ARM64

AAPCS64

iOS

  ARM64

AAPCS64 + Apple extensions

Linux

  x86-64

System V ABI

macOS

  x86-64

System V + Apple differences

Windows

  x86-64

Microsoft x64 ABI

Linux

  ARM32

ARM EABI

Embedded

  ARM Thumb

   AAPCS

So yes – ABIs differ across OSes and CPUs.

Then how do different languages call each other?

They don’t choose their ABI individually.
The OS defines the ABI. Every runtime and compiler on that OS follows it.

Thus:

  • Dart FFI follows the OS ABI
  • JVM native calls follow the OS ABI
  • Python C extensions follow the OS ABI
  • Swift runtime follows the OS ABI
  • Node.js native modules follow the OS ABI

This is why a C function compiled on Linux x86-64 cannot be loaded into an iOS ARM64 program: the ABIs are completely incompatible.

4. The Universal Pattern of Calling Conventions

Regardless of OS, ABI always defines:

✔ Which registers hold the first N arguments

Examples:

ARM64 (iOS/Android)

integer/pointer → x0–x7
float → v0–v7

x86-64 (Linux/macOS)

RDI, RSI, RDX, RCX, R8, R9

x86-64 (Windows)

RCX, RDX, R8, R9

✔ How additional arguments go on stack

Aligned to 16 bytes on ARM64
Aligned to 8 bytes on x86-64

✔ Return values

ARM64 → x0 (int/pointer), v0 (float)
x86-64 → RAX, XMM0

✔ How stack frames look

Every OS + CPU has a strict stack layout.

✔ Register saving discipline

caller-saved
callee-saved

This prevents chaos between function boundaries.
Without this, a single function call could corrupt memory anywhere.

5. Example: Same C Function on Different ABIs
				
					int add(int a, int b) {
    return a + b;
}
				
			

iOS/Android (ARM64) ABI

				
					int add(int a, int b) {
    return a + b;
}
				
			

Linux/macOS (x86-64 System V)

				
					int add(int a, int b) {
    return a + b;
}
				
			

Windows x86-64 ABI

				
					int add(int a, int b) {
    return a + b;
}
				
			

Same code, three different ABIs.

6. Cross-Runtime Example: JVM → C (JNI)

Java:

				
					native int add(int a, int b);
				
			

 At runtime:

  1. JVM collects arguments into registers according to the OS ABI

  2. Calls the C function using ARM64 ABI or System V ABI

  3. Reads the return value from the proper register

JVM does NOT define its own ABI. It strictly follows the OS ABI.

7. Swift/Objective-C Interop Uses ABI

Swift and Objective-C both follow the Apple ARM64 ABI.

That’s why:

				
					@_cdecl("add")
func add(_ a: Int32, _ b: Int32) -> Int32 {
    return a + b
}
				
			

 Can be called directly from C, C++, Dart (via FFI), Python, Rust, etc.

They all rely on ABI.

8. Dart FFI → Same ABI

When Dart calls C:

  1. Dart VM loads arg1 into x0

  2. Dart VM loads arg2 into x1

  3. Performs BL (branch with link)

  4. Reads return value from x0

Exactly like C, Java, Swift, Rust, Python…

Because all runtimes follow the same ABI.

9. How Do Two Runtimes Communicate? (Generalized Explanation)

Step 1: Language → Runtime

Dart/JVM/Swift/Python transforms your function call into an internal runtime call.

Step 2: Runtime → Native Stub

Runtime generates a small piece of machine code that translates high-level data types into ABI-compatible raw values:

1. ints → registers
2. doubles → FP registers
3. structs → memory blocks
4. strings → pointers

Step 3: Native Stub → ABI Call

The stub follows the OS ABI:

 1. pushes stack frames
 2. aligns stack
3. fills argument registers
4. jumps to native function

Step 4: Callee Executes

Native code runs normally in its own stack frame.

Step 5: Return Value

Returned in ABI-defined register (x0, v0, RAX, etc.)

Step 6: Runtime Receives Raw Data

Converts raw binary representation back to high-level types.

10. Why ABIs Make Cross-Language Interop Possible

Because all languages ultimately:

  •  Use the same register conventions
  • Use the same stack alignment rules
  • Use the same return conventions
  • Use the same struct memory layouts
  • Follow the same dynamic linking format
  • Follow the same name mangling rules

This is why:

  •  Python can load C modules
  • Java can call C via JNI
  • Node.js can load native addons
  • Flutter can load .so and .framework files
  • Swift and Objective-C interoperate
  • Rust can call C or be called from Swift
  • Go can call or export C functions
  • Unreal Engine (C++) integrates Lua, Python, Blueprints
  • Unity (C#) calls native plugins
11. The ABI Is NOT C - It’s Lower Than C

C is just the most common language that maps cleanly to ABI rules : 

  • no hidden metadata
  • simple structs
  • no exceptions
  • no dynamic dispatch
  • simple symbol names

This is why the world uses C ABI rather than Java ABI or Swift ABI.

12. Final Summary

### ABI = the universal binary contract of all runtimes.

It specifies:

  • how arguments are passed
  • how return values work
  • how stack frames look
  • how registers are used
  • how memory is aligned
  • how functions are named
  • how exceptions unwind

All languages must obey the ABI of the OS they’re running on.

This guarantees interoperability between:

  • Dart ↔ C
  • Java ↔ C++
  • Swift ↔ Objective-C
  • Python ↔ C extensions
  • Rust ↔ Swift
  • Node.js ↔ C++ native modules
  • Go ↔ everything
  • Flutter ↔ Engine

Different OS+CPU = different ABI

Linux x86-64 ABI ≠ iOS ARM64 ABI ≠ Windows ABI.

But each platform enforces one ABI, so all languages can cooperate.

Md. Ahsanul Haque

29 June, 2026