The difference that matters at runtime
API is a source-level contract: a function has a name, a signature, a set of arguments. If your code calls DrawIndexed and the library exposes DrawIndexed, a compiler can wire them together. ABI is what survives compilation — the binary contract that specifies which register carries the first argument, how a struct is laid out in memory, how the stack is aligned when control transfers, and what calling convention governs cleanup. By the time a program is running, the human-readable names are largely gone. What remain are addresses, offsets, and sequences of bytes the CPU will execute.
A compatibility layer operating at the syscall boundary deals with this constantly. The program was compiled against one ABI — a particular struct layout for a surface descriptor, a particular order of fields in a resource handle — and the layer must present an identical binary shape, not merely a matching name. Get the padding wrong by four bytes and the program reads a flag from what is actually an integer field; it does not crash immediately, it behaves strangely in a way that takes hours to trace.
Mechanisms worth pulling out
FROM THIS ENTRY| Calling convention difference (Windows x64 vs System V AMD64) | registers RCX/RDX/R8/R9 vs RDI/RSI/RDX/RCX/R8/R9 |
|---|---|
| Struct padding: same field names, different byte widths across compilers or platforms | |
| ABI vs API distinction: source-level name vs binary-level shape, offsets, and register assignments |
Calling convention is the place this bites hardest. Windows x64 code passes the first four integer arguments in RCX, RDX, R8, and R9, then spills to the stack. The System V AMD64 ABI used on Linux passes the first six in RDI, RSI, RDX, RCX, R8, and R9. The functions can have identical names and identical semantics; a call crossing that boundary without translation lands the arguments in the wrong registers and the callee reads garbage. The layer has to know which convention the compiled code expects and present itself accordingly — every time, for every call.
Struct packing adds a further layer of fragility. Compilers insert padding between fields to satisfy alignment requirements, and those requirements can differ by platform, by compiler version, and by pragma. A struct that is 48 bytes wide on the target ABI and 52 bytes wide on the host will be misread silently. The layer has to either match the original layout exactly or copy field by field across the boundary, which is slower and has to be maintained as the interface evolves.

This is why translation is painstaking work far below the level of function names.

