← Back to Notes

Demystifying Cross-Language Runtimes: How FFI, Shared Memory, and the C-ABI Power Modern Software

2026-05-24

By @boooshir, @google ai

image In the modern software ecosystem, we are rarely locked into a single programming language. Python dominates machine learning, JavaScript and TypeScript rule the web, while Rust, C++, and Zig power low-level systems.

But have you ever wondered how these runtimes actually talk to each other? When Bun executes a TypeScript file at lightning speed, or when Python loads a Rust module to crunch millions of data points, what is happening behind the scenes?

The answer lies in a universal hardware-level contract: The C-ABI (Application Binary Interface) and FFI (Foreign Function Interface). ---## 1. The Core Architecture: Shared Process Memory When a server starts a multi-language application (e.g., Python running Rust, or Bun running Zig), the Operating System creates exactly one process. This process is allocated a single sandbox of virtual memory (RAM).

┌────────────────────────────────────────────────────────┐
│ [ PHYSICAL PROCESS SYSTEM RAM BUFFER ]                 │
├────────────────────────────────────────────────────────┤
│ 💾 DATA LAYOUT (C-ABI Contract)                        │
│ [Address: 0x1000] ──> [ 42 ][ 100 ][ "Hello\0" ]       │
│ ▲                                                      │
│ ├─ Language A (Python/JS) reads these raw bytes        │
│ └─ Language B (Rust/Zig) writes directly to them       │
├────────────────────────────────────────────────────────┤
│ ⚡ CODE PATHS (FFI Function Execution)                 │
│ [Address: 0x5000] ──> Compiled CPU Instructions        │
│ ▲                                                      │
│ └─ Language A commands the CPU to jump to 0x5000       │
└────────────────────────────────────────────────────────┘

CPUs do not understand what "JavaScript," "Python," or "Rust" are. They only understand binary machine code instructions and raw memory addresses. For two languages to communicate inside the same process without crashing, they must agree on two things:

  1. How data is organized in RAM (Data Structures)
  2. How functions are triggered (Execution Paths)

2. The Universal Law Book: What is the C-ABI?

Every language has its own internal rules for organizing memory. For example, the Rust compiler will naturally scramble the order of fields inside a structure to optimize space. If you pass that memory structure directly to C++, C++ will look for data in the wrong bytes, causing a critical crash (Segmentation Fault).

To prevent this, compilers use the C Application Binary Interface (C-ABI). The C-ABI forces a language to lower its guard and layout data exactly like a standard C compiler would.

The Background Mechanics of

When you declare in C++, Rust, or Zig, you are enforcing a strict compilation contract that alters two critical behaviors:

A. Disabling Name Mangling

Compilers normally scramble function names into complex internal hashes to support features like method overloading.

  • Native C++ name: becomes .
  • Native Rust name: becomes .

By applying (and in Rust), you force the compiler to save the function name in the final compiled binary exactly as written: . This allows external linkers to find the function instantly.

B. Standardizing the Calling Convention

The C-ABI defines the exact hardware-level rules for how the CPU handles a function call:

  • It dictates which specific CPU registers (like , , ) hold incoming arguments.
  • It defines whether the caller or the function cleans up the CPU stack afterward.

Because all major languages know how to mimic C's hardware-level rules, they can pass variables directly through CPU registers without any middleman translation layer.


3. High-Speed Handoffs: How FFI Triggers Functions

Foreign Function Interface (FFI) is the mechanism that allows one language to execute a function written in another. Under the hood, this is surprisingly simple: languages trade Function Pointers.

A function pointer is merely a 64-bit memory address pointing to the first instruction of compiled machine code in RAM.

Zero-Copy Pointer Handoffs

When Language A passes a massive database array or file buffer to Language B over FFI, it doesn't duplicate the data. It simply places a 64-bit virtual memory address register on the CPU and says: "The data starts at address . Go read it directly." This is called Zero-Copy memory sharing.


4. Deep Dive: How Modern JavaScript Runtimes Run

We can see this exact FFI architecture deployed at massive scale inside modern JavaScript runtimes. While developers write JavaScript/TypeScript, the runtimes themselves are compiled system binaries.


┌─────────────────┐ Transpile (RAM)     ┌─────────────────┐
│ User's app.ts   |────────────────────>│ Pure app.js     │
└─────────────────┘                     └────────┬────────┘
│
▼ Boots Engine
┌─────────────────┐ Zero-Copy Pointers  ┌─────────────────┐
│ OS Kernel       │<────────────────────│ System Engine   │
│ (Disk/Network)  │                     │ (C++/Rust/Zig)  │
└─────────────────┘                     └─────────────────┘

The Architecture Matrix


| Runtime     | The Gateway (JS Engine) | The System Layer (Worker Engine) |
| ----------  | ----------------------  | -------------------------------  |
| **Node.js** | Google V8 (C++)         | libuv (C)                        |
| **Deno**    | Google V8 (C++)         | Tokio (Rust)                     |
| **Bun**     | JavaScriptCore (C++)    | Bun Core (Zig)                   |

Every single time a JavaScript runtime executes a file or hits an API, it follows a strict lifecycle path:

  1. Initialization: The OS loads the compiled runtime binary (e.g., the Bun binary written in Zig).
  2. In-Memory Transpilation: If running TypeScript, Bun’s built-in transpiler (written in Zig) instantly strips out types and converts the file to clean JavaScript strictly in the RAM buffer.
  3. The Gateway Handshake: The runtime boots up its JavaScript Engine (V8 or JavaScriptCore) and injects global APIs (like or ) as tiny C-ABI function pointers.
  4. Independent CPU Execution: When your JavaScript code calls an asynchronous I/O method, the engine extracts the memory pointer of your arguments and fires them across the FFI bridge. The main JavaScript thread is freed up, while the fast background system layer (written in Zig, Rust, or C++) talks directly to the OS kernel ( or ) to perform bare-metal hardware operations.

5. The Performance Trade-Off: Understanding FFI Overhead

While FFI allows us to run code at bare-metal speeds, it is not a silver bullet. Crossing the language barrier introduces a minor penalty known as FFI Call Overhead.

Before a function can jump across languages, the host environment must pause its runtime loop, safely unwrap its high-level objects (like converting a Python or V8 JS Variable) into raw hardware integer/string pointers, and execute safety boundary checks. This process takes roughly 30 to 60 nanoseconds per call.

The Workload Golden Rule

  • A Simple Function (Winner: Native Language): If a function merely adds two numbers together, the time spent crossing the FFI boundary outweighs the calculation time. Running it natively within the host script is faster.
  • A Heavy Workload (Winner: FFI Module): If a function contains massive loops, cryptographic calculations, or data transformations, the 60-nanosecond boundary transition fee becomes completely invisible. The logic inside the compiled FFI library runs 10x to 100x faster, bypassing interpreter bottlenecks and utilizing true parallel CPU processing.

Simple Math: [ FFI Transition Overhead (60ns) ] + [ Math (5ns) ] = 65ns (Slower) Heavy Loop: [ FFI Transition Overhead (60ns) ] + [ Loop (2ms) ] = ~2ms (100x Faster)

The Strategy: Never call an FFI function millions of times inside a high-level loop. Instead, pass your raw dataset once across the bridge, let your compiled backend run the heavy operations entirely in native memory, and pass the final result back once.


6. Conclusion

Every cross-language architecture on a server functions identically under the hood. Whether it is Node.js talking to C++, Deno invoking Rust, Bun interacting with Zig, or Python executing PyO3 bindings, they are all utilizing the same foundational concepts. By standardizing memory via the C-ABI and mapping execution pipelines via FFI, modern engines seamlessly share memory pointers and CPU registers—giving developers the ease of high-level coding alongside the unmatched speed of bare metal.


To continue expanding this documentation, let me know if you would like me to add code blocks demonstrating a custom native module setup (such as a full Rust PyO3 or Zig bun:ffi script) to serve as a practical code appendix.