8/23/2026

API and ABI, and the line between them

C · Python · Linking

Two acronyms one letter apart, describing promises made at two different moments. An API is what you agree to when you write the call. An ABI is what your compiled bytes agree to when they meet somebody else's compiled bytes — and unlike the API, breaking it usually produces no error at all.

The clearest way in is to put them side by side. Only the middle word differs: Programming or Binary.

AcronymLevelA promise that sounds like
API
Application Programming Interface
source code "there is a function resize(img, width, height) and it returns an int"
ABI
Application Binary Interface
machine code and memory "width is at byte 0 and is 4 bytes; the first argument arrives in register rdi"

An API is a contract between a programmer and a library. An ABI is a contract between two piles of already-compiled machine code. That is why two languages can call each other at all: Python has no idea what a C header looks like, but if it lays the bytes out the way the ABI says and puts the arguments where the ABI says, the call works.

This post is in five parts. First the distinction and what an ABI actually specifies. Then the three pieces you can see and measure — struct layout, symbol names, argument registers — each with a program you can run. Then the same boundary crossed from Python, including two mistakes that produce wrong answers in total silence. Finally, what it takes to keep an ABI stable once other people depend on it.

Everything was compiled and run before publishing: gcc 11.4.0 on x86-64 Linux, CPython 3.10.12, GNU binutils 2.38. Every output block is copied from a real run.

Contents

  1. Two contracts — the distinction, and why it costs you a rebuild
  2. Struct layout — the part you can print out
  3. Symbols and registers — what the linker sees, and where arguments go
  4. Crossing from Python — where a mismatch stops being theoretical
  5. Keeping an ABI — what breaks what, and how libraries survive it
  6. Reference — tools, cheat sheet, and a one-screen summary

Part 1 · Two contracts

What each one promises

Take an ordinary header. It declares a struct and a function:

the API, as written
struct config {
    int    width;
    int    height;
    double scale;
};

int config_area(const struct config *c);

Everything visible there is API: the names, the types, the order of the parameters, the fact that the function takes a pointer rather than a value. If any of it changes, code that calls it stops compiling — loudly, with a file and a line number.

The ABI is everything the compiler decided underneath that text, and none of it appears in the header: that width begins at byte 0, that scale begins at byte 8 and not byte 12, that the whole struct occupies 16 bytes, that the pointer argument arrives in the register rdi, that the int comes back in eax, and that the symbol is spelled config_area rather than _Z11config_areaPK6config.

Those decisions are just as much a contract, and both sides are bound by them. The difference is what happens when one side changes its mind.

Recompile, relink, neither

The practical consequence of the distinction is what a change costs the people who use your library.

ChangeAPIABIWhat users must do
Fix a bug inside a function body intactintact Nothing. Drop in the new .so.
Add a brand-new function intactintact Nothing, unless they want the new one.
Rename a function brokenbroken Edit their source, rebuild. The compiler tells them.
Reorder two struct fields intactbroken Rebuild — and nothing tells them.
Change a field from int to long mostly intactbroken Rebuild. Again, silently.

The last two rows are the whole reason this topic matters. A broken API is a compiler error; a broken ABI is a program that runs and gives you the wrong number. The rest of this post is mostly about making that second row concrete enough that you can spot it.

What an ABI decides

For a plain C interface on one machine, the list is short and worth memorising:

  1. Struct layout — which byte each field starts at, and how big the whole thing is. Part 2.
  2. Symbol naming — what the function is called in the object file. Part 3.
  3. The calling convention — which register or stack slot each argument travels in, and who cleans up. Part 3.
  4. Return values — which register the result comes back in, which depends on its type.

Sizes and alignments of the basic types sit underneath all four. C only guarantees relationships — that a long is at least as wide as an int — and the ABI picks the actual numbers, which is why long is 8 bytes on Linux and 4 bytes on 64-bit Windows.

Naming

The convention used throughout this post is the x86-64 System V ABI, which is what Linux and macOS use. Windows has a different one, as do 32-bit x86 and the various ARM targets. The concepts transfer exactly; the specific registers do not.

Part 2 · Struct layout

Where the fields actually land

You do not have to guess at any of this. offsetof and sizeof report what the compiler decided, and _Alignof reports the rule it was following.

layout.c — same fields, two orders
struct badly_ordered {
    char   tag;
    double value;
    int    id;
};

struct well_ordered {
    double value;
    int    id;
    char   tag;
};

#define SHOW(S, M)                                                       \
    printf("  %-6s offset %2zu   size %2zu   align %2zu\n", #M,          \
           offsetof(S, M), sizeof(((S *)0)->M), _Alignof(((S *)0)->M))
./layout
struct badly_ordered   sizeof = 24   align 8
  tag    offset  0   size  1   align  1
  value  offset  8   size  8   align  8
  id     offset 16   size  4   align  4

struct well_ordered    sizeof = 16   align 8
  value  offset  0   size  8   align  8
  id     offset  8   size  4   align  4
  tag    offset 12   size  1   align  1

field bytes actually used: 13
padding in badly_ordered : 11
padding in well_ordered  : 3

Alignment and padding

Two rules produce every number in that output.

Each field starts at a multiple of its own alignment. A double has alignment 8, so it may only begin at byte 0, 8, 16 and so on. In badly_ordered, tag occupies byte 0, and value cannot start at byte 1 — the compiler skips to byte 8 and leaves seven bytes of nothing behind.

The struct's own size is rounded up to its largest field alignment. That is what makes arrays work: if sizeof were not a multiple of 8, the second element of an array would put value on a misaligned address. In badly_ordered, id ends at byte 20 and the size rounds up to 24.

The two structs contain identical data — a double, an int and a char, 13 bytes of payload — and one is 50% larger than the other. Sorting fields from widest to narrowest, as well_ordered does, is a reliable way to get most of that back.

Note

Padding bytes have no defined value. This is why memcmp on two structs can report a difference when every field is equal, and why writing a struct straight to a file can leak whatever happened to be on the stack.

Reordering is an ABI change with no API change

Now put the two structs next to each other again and ask what a caller sees. Both have the same three fields with the same names and the same types. Any code that says c.value = 1.5 compiles unchanged against either one. The API is identical.

The bytes are not. Field value lives at offset 8 in one and offset 0 in the other. A library compiled against the first and a caller compiled against the second will agree on every name and disagree on every address — and no tool in the build will say a word, because each half is internally consistent.

Trap

sizeof matching is not evidence that two layouts agree. Two different orderings can easily produce the same total size — you will see exactly that in part 4, where a reordered struct is 16 bytes on both sides and still reads the wrong bytes.

Part 3 · Symbols and registers

Name mangling

A linker does not match calls to functions by reading your source. It matches strings. What string a function gets is part of the ABI, and C and C++ answer that question very differently.

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

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

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

extern "C" int add_c(int a, int b) { return a + b; }
g++ -c mangling.cpp -o mangling.o && nm mangling.o
0000000000000018 T _Z3adddd
0000000000000000 T _Z3addii
0000000000000040 T _ZN8geometry3addEii
0000000000000058 T add_c

Three functions are all called add in the source and none of them is called add in the object file. C++ encodes the namespace and the parameter types into the symbol — that encoding is what makes overloading possible, since the linker needs a different string for each version. _Z3addii is add taking two ints; _Z3adddd is the double one.

nm -C undoes it, as does piping any symbol through c++filt:

nm -C mangling.o
0000000000000018 T add(double, double)
0000000000000000 T add(int, int)
0000000000000040 T geometry::add(int, int)
0000000000000058 T add_c

What extern "C" is for

The fourth symbol is the odd one out: add_c, spelled exactly as written. That is the entire effect of extern "C" — it tells a C++ compiler to use the C naming rule (the plain name) and the C calling rule for this function, so that anything outside C++ can find it.

It is not a performance hint and it does not change the function body. It is a promise about the string in the symbol table, which is why every library meant to be called from another language has a wall of it around its public header. And it is why C++ features that need mangling — overloads, templates, namespaces — cannot appear on such a boundary: there is only one add_c available, so there can only be one function of that name.

Look at the exported symbols of a C library and you get the flat list you would expect:

nm -D --defined-only libconfig.so
0000000000001119 T config_area
0000000000001174 T config_describe
00000000000011de T config_offset_scale
0000000000001137 T config_scaled_area
00000000000011cf T config_sizeof

That list, plus the layout of struct config, is the library's entire ABI surface. The T means the symbol is defined here and visible to others; U would mean undefined and needed from somewhere else.

The calling convention

The last piece is where arguments physically go. Three tiny functions and a disassembler answer it directly.

callconv.c
int sum4(int a, int b, int c, int d) { return a + b + c + d; }

double product2(double x, double y) { return x * y; }

int mixed(int a, double x, int b) { return a + b + (int)x; }
gcc -O1 -c callconv.c && objdump -d --no-show-raw-insn callconv.o
0000000000000000 <sum4>:
   0:	endbr64
   4:	add    %esi,%edi
   6:	add    %edx,%edi
   8:	lea    (%rdi,%rcx,1),%eax
   b:	ret

000000000000000c <product2>:
   c:	endbr64
  10:	mulsd  %xmm1,%xmm0
  14:	ret

0000000000000015 <mixed>:
  15:	endbr64
  19:	add    %esi,%edi
  1b:	cvttsd2si %xmm0,%eax
  1f:	add    %edi,%eax
  21:	ret

There is no prologue, no stack, no memory traffic at all. In sum4 the four arguments are already sitting in edi, esi, edx and ecx when the function begins, and the result is left in eax. That order is not something gcc invented; it is written down in the ABI, and every compiler targeting this platform obeys it.

KindArguments 1–6Beyond thatReturn
Integers and pointers rdi rsi rdx rcx r8 r9 pushed on the stack rax
Floats and doubles xmm0xmm7 pushed on the stack xmm0

product2 confirms the second row: mulsd %xmm1,%xmm0 multiplies the two arguments in place and the result is already in the return register.

The detail that surprises people

Look again at mixed(int a, double x, int b). The instruction is add %esi,%edi — so a is in edi and b is in esi, even though b is the third parameter.

The two register sequences are counted independently. Integers take rdi, rsi, … in order among themselves; floats take xmm0, xmm1, … in order among themselves. The double in the middle consumes xmm0 and does not consume an integer slot, so b gets the next integer register rather than the third one.

Why you care

Because it means you cannot work out where an argument goes by counting parameters. Any hand-written declaration that gets a parameter's type wrong — say, declaring a double parameter as int in an FFI binding — does not merely mistranslate that one value. It shifts every argument after it into the wrong register.

Part 4 · Crossing over from Python

The library

Everything so far has been observation. This part is where a mismatch actually costs you, and the most convenient way to build one is a C library and a Python client, because Python has no access to the header and has to be told the layout by hand.

libconfig.c
struct config {
    int    width;
    int    height;
    double scale;
};

int config_area(const struct config *c) {
    return c->width * c->height;
}

const char *config_describe(const struct config *c) {
    static char buf[64];
    snprintf(buf, sizeof buf, "%dx%d @ %.2fx", c->width, c->height, c->scale);
    return buf;
}

/* Self-description, so a caller can verify its mirror instead of trusting it. */
size_t config_sizeof(void) { return sizeof(struct config); }
size_t config_offset_scale(void) { return offsetof(struct config, scale); }

Those last two functions are worth a moment. They export nothing useful to a program — but they let a caller check its assumptions instead of hoping. A library that describes its own layout turns a silent class of bug into an assertion, for the price of two lines.

A mirror that matches

On the Python side, ctypes needs the layout restated. This declaration is a hand-copy of the C struct, and the whole exercise depends on it being right.

client.py
class Config(ctypes.Structure):
    _fields_ = [
        ("width", ctypes.c_int),
        ("height", ctypes.c_int),
        ("scale", ctypes.c_double),
    ]

lib.config_area.argtypes = [ctypes.POINTER(Config)]
lib.config_area.restype = ctypes.c_int

assert ctypes.sizeof(Config) == lib.config_sizeof()
assert Config.scale.offset == lib.config_offset_scale()
python3 client.py — part 1
== 1. matching mirror ==
sizeof        : python 16 | c 16
offset(scale) : python 8 | c 8
area          : 2073600
describe      : 1920x1080 @ 1.50x

Python computed offset 8 for scale by applying the same alignment rules the C compiler applied, independently, from the field types alone. That is not a coincidence — ctypes implements the platform ABI. It is also the reason the next section works the way it does.

A mirror that doesn't

Now the realistic accident: somebody writes the same three fields in a different order. Perhaps they copied from documentation that listed them alphabetically. Every name is right, every type is right.

client.py — the same fields, reordered
class ConfigWrong(ctypes.Structure):
    _fields_ = [
        ("scale", ctypes.c_double),
        ("width", ctypes.c_int),
        ("height", ctypes.c_int),
    ]
python3 client.py — part 2
== 2. reordered mirror ==
sizeof        : python 16 | c 16 -> agrees, and proves nothing
offset(scale) : python 0 | c 8 -> the actual disagreement
area          : 0
describe      : 0x1073217536 @ 0.00x
no exception, no warning, no crash

A 1920×1080 image now has an area of zero, and a height of 1073217536. Those numbers are not random: C read width from bytes 0–3, which in this layout hold the bottom half of the double 1.5, and that half is all zeros. It read height from bytes 4–7, the top half of the same double, which is 0x3FF80000 — the number printed.

Three things about that output deserve to be uncomfortable. There is no exception. There is no warning. And sizeof agrees perfectly on both sides, because reordering these particular fields happens to produce the same total. A size check would have passed this straight through; only the offset check catches it.

Trap

ctypes type-checks the Python side thoroughly and the C side not at all. It will refuse a ConfigWrong where you declared POINTER(Config) — but it has no way to know what the library thinks either of them looks like. Every guarantee stops at the boundary.

The restype trap

The other everyday mistake is forgetting to declare what comes back. ctypes assumes a C int unless told otherwise, and part 3 of the calling convention says an int comes back in rax while a double comes back in xmm0. Two different registers.

python3 client.py — part 3
== 3. missing restype ==
default (int) : 0
c_double      : 3110400.0
expected      : 3110400.0

The first call reads an integer register that the function never wrote to. It printed 0 here and it will print whatever is left over there on your machine; the point is only that it has nothing to do with the answer.

The same failure with a pointer return is worse. Left as the default int, a returned pointer is truncated to its low 32 bits, and dereferencing the result is a segmentation fault that appears to come from inside the C library. Setting argtypes and restype on every function you import is not ceremony — it is the only place the ABI gets written down on the Python side.

What the Python examples actually demonstrate

  • An FFI binding is a hand-copy of an ABI. Nothing checks it against the library, so it drifts the moment the library changes.
  • A matching sizeof proves very little. Compare offsets, field by field, or compare nothing.
  • Have the library describe itself. Two exported functions turned an invisible failure into an assert that fires on line one.

Part 5 · Keeping an ABI stable

What breaks which

Once other people ship binaries built against your library, every change falls into one of these boxes. The dangerous column is the one where the API survives and the ABI does not, because that is the change nobody notices.

Change to a public struct or functionAPIABI
Add a functionokok
Change a function bodyokok
Rename a parameter in the headerokok
Reorder struct fieldsokbroken
Insert a field in the middleokbroken
Widen a field, intlongokbroken
Add a field at the endokbroken if callers allocate it
Change a parameter's typebrokenbroken
Remove or rename a functionbrokenbroken
Change a #define constantbehaviour changesok

"Add a field at the end" is the interesting one. If callers only ever receive a pointer your library allocated, growing the struct is harmless — they never compute its size. If callers declare one on their own stack, they will allocate the old, smaller size, and your library will write past the end of it.

How libraries stay compatible

Hide the struct entirely

The strongest option: callers never see the fields, only a pointer to something they cannot inspect.

opaque handle
/* in the public header */
typedef struct config config_t;        /* declared, never defined */

config_t *config_create(void);
void      config_set_scale(config_t *c, double scale);
int       config_area(const config_t *c);
void      config_destroy(config_t *c);

The layout is now a private implementation detail — it can change in any release, because no caller ever computed an offset into it. The cost is a function call per field and an allocator round trip. This is how most C libraries with long support windows do it.

Put the size in the struct

When callers must allocate, have them declare how big their version is, and check.

versioned struct
struct config {
    size_t struct_size;    /* caller sets this to sizeof(struct config) */
    int    width;
    int    height;
    double scale;
    /* new fields may only be appended below */
};

int config_area(const struct config *c) {
    if (c->struct_size < offsetof(struct config, scale)) return -1;
    ...
}

The library can now tell an old caller from a new one at runtime and read only the fields that caller actually has. The rule is absolute, though: fields may only ever be appended, and existing ones may never move or change width.

Reserve space up front

A cruder version of the same idea — char reserved[32]; at the end of the struct, spent later on new fields without changing the size. It works, it is ugly, and it is everywhere.

Version the symbol, not the struct

When a function genuinely has to change shape, keep the old one and add a new name: config_area stays, config_area2 appears. Old binaries keep finding the old symbol. The C standard library has done this for decades, which is why symbols with version suffixes turn up in nm output on any mature library.

soname, and how the loader picks a file

Shared libraries carry a name inside them, and it is that name — not the filename — that gets recorded in every program linked against them.

building with a soname
gcc -shared -fPIC -Wl,-soname,libconfig.so.1 -o libconfig.so.1.0.0 libconfig.c
readelf -d libconfig.so.1.0.0 | head -6
readelf -d libconfig.so.1.0.0
Dynamic section at offset 0x2e10 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libconfig.so.1]
 0x000000000000000c (INIT)               0x1000

The convention that falls out of this is the familiar three-level filename. The file is libconfig.so.1.0.0; the soname is libconfig.so.1, and that is what programs record; the bare libconfig.so is a symlink that only the linker uses at build time.

The middle number is the promise. Bump the patch level for a bug fix, bump the minor for added functions, and bump the soname to .so.2 when the ABI breaks. Because the soname is part of the filename, version 1 and version 2 can sit in the same directory and old programs keep loading the one they were built against. That is the whole mechanism by which a Linux system runs binaries compiled years apart.

Part 6 · Reference

Tools worth knowing

nm -D --defined-only lib.soExported symbols — the callable ABI surface
nm -C, c++filtDemangle C++ symbol names
readelf -d lib.sosoname and the list of libraries it needs
readelf -Ws lib.soFull symbol table with sizes and visibility
objdump -d --no-show-raw-insn f.oDisassembly — see the calling convention directly
ldd ./programWhich libraries actually get loaded, and from where
gcc -fdump-lang-raw, paholeStruct layout and padding, reported by the toolchain
offsetof, _Alignof, _Static_assertAssert the layout from inside the program itself

Cheat sheet

The distinction

APISource-level contract. Broken → compiler error, with a line number.
ABIBinary-level contract. Broken → wrong answers, usually with no message at all.

x86-64 System V, in four lines

Integer / pointer argumentsrdi rsi rdx rcx r8 r9, then the stack
Float / double argumentsxmm0xmm7, counted separately
Return valuerax, or xmm0 for floating point
Struct field offsetsEach field aligned to its own width; total rounded up to the largest

Writing an FFI binding

Set argtypes and restypeOn every single function. The defaults are wrong for pointers and doubles.
Assert sizeof and offsetsSizes can match while layouts differ.
Export a self-descriptionTwo functions returning sizeof and an offsetof pay for themselves.
Prefer opaque pointersA layout nobody can see is a layout nobody can get wrong.

Wrapping up

What to take away

The mnemonic in the first table is worth keeping — Programming versus Binary, one contract for people writing code and one for code that has already been written. But the reason to care is narrower than that.

The short list

  • An API break is loud; an ABI break is silent. The compiler is on your side for one of them and completely absent for the other.
  • Field order is part of your public interface. It does not look like it in the header, and swapping two lines is a breaking change.
  • Every hand-written binding is a copy of a layout, so make it checkable. Export sizeof and a couple of offsets, assert them at import time, and the entire category of bug in part 4 turns into a failed assertion.

The stability techniques in part 5 only matter once someone else ships a binary against your code. Until then the ABI is a private matter and you can rearrange whatever you like. The day that stops being true is the day the opaque pointer starts looking cheap.

Further reading

Appendix · The complete programs

Building and running

Five short files produced every output on this page. They are reproduced in full below, so copying the snippets into one directory is enough to reproduce all of it.

build and run everything
gcc -std=c11 -Wall -Wextra -o layout layout.c && ./layout

gcc -std=c11 -Wall -Wextra -shared -fPIC -o libconfig.so libconfig.c
python3 client.py

g++ -c mangling.cpp -o mangling.o && nm mangling.o && nm -C mangling.o

gcc -O1 -c callconv.c -o callconv.o
objdump -d --no-show-raw-insn callconv.o

gcc -shared -fPIC -Wl,-soname,libconfig.so.1 -o libconfig.so.1.0.0 libconfig.c
readelf -d libconfig.so.1.0.0 | head -6

Verified on gcc 11.4.0 and GNU binutils 2.38 (Ubuntu 22.04, x86-64) with CPython 3.10.12. The one value that is not reproducible is the integer read in part 3 of client.py, which is whatever the return register happened to contain.

layout.c 43 lines

layout.c
#include <stddef.h>
#include <stdio.h>

/* Same three fields, same API, two different binary layouts. */

struct badly_ordered {
    char   tag;
    double value;
    int    id;
};

struct well_ordered {
    double value;
    int    id;
    char   tag;
};

#define SHOW(S, M)                                                       \
    printf("  %-6s offset %2zu   size %2zu   align %2zu\n", #M,          \
           offsetof(S, M), sizeof(((S *)0)->M), _Alignof(((S *)0)->M))

int main(void) {
    size_t payload = sizeof(double) + sizeof(int) + sizeof(char);

    printf("struct badly_ordered   sizeof = %2zu   align %zu\n",
           sizeof(struct badly_ordered), _Alignof(struct badly_ordered));
    SHOW(struct badly_ordered, tag);
    SHOW(struct badly_ordered, value);
    SHOW(struct badly_ordered, id);

    printf("\nstruct well_ordered    sizeof = %2zu   align %zu\n",
           sizeof(struct well_ordered), _Alignof(struct well_ordered));
    SHOW(struct well_ordered, value);
    SHOW(struct well_ordered, id);
    SHOW(struct well_ordered, tag);

    printf("\nfield bytes actually used: %zu\n", payload);
    printf("padding in badly_ordered : %zu\n",
           sizeof(struct badly_ordered) - payload);
    printf("padding in well_ordered  : %zu\n",
           sizeof(struct well_ordered) - payload);
    return 0;
}

callconv.c 9 lines

callconv.c
/* Where do arguments actually go? Build and look:
     gcc -O1 -c callconv.c -o callconv.o
     objdump -d --no-show-raw-insn callconv.o */

int sum4(int a, int b, int c, int d) { return a + b + c + d; }

double product2(double x, double y) { return x * y; }

int mixed(int a, double x, int b) { return a + b + (int)x; }

mangling.cpp 12 lines

mangling.cpp
/* What a linker actually sees. Build with:
     g++ -c mangling.cpp -o mangling.o && nm mangling.o */

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

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

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

extern "C" int add_c(int a, int b) { return a + b; }

libconfig.c 29 lines

libconfig.c
#include <stddef.h>
#include <stdio.h>

/* Everything a caller in another language has to agree with lives here:
   the layout of struct config, and the exported symbols below. */

struct config {
    int    width;
    int    height;
    double scale;
};

int config_area(const struct config *c) {
    return c->width * c->height;
}

double config_scaled_area(const struct config *c) {
    return c->width * c->height * c->scale;
}

const char *config_describe(const struct config *c) {
    static char buf[64];
    snprintf(buf, sizeof buf, "%dx%d @ %.2fx", c->width, c->height, c->scale);
    return buf;
}

/* Self-description, so a caller can verify its mirror instead of trusting it. */
size_t config_sizeof(void) { return sizeof(struct config); }
size_t config_offset_scale(void) { return offsetof(struct config, scale); }

client.py 76 lines

client.py
"""Three ways to talk to libconfig.so: one right, two wrong."""

import ctypes

lib = ctypes.CDLL("./libconfig.so")
lib.config_sizeof.restype = ctypes.c_size_t
lib.config_offset_scale.restype = ctypes.c_size_t

# ---- 1. a mirror that matches the C struct -------------------------------

class Config(ctypes.Structure):
    _fields_ = [
        ("width", ctypes.c_int),
        ("height", ctypes.c_int),
        ("scale", ctypes.c_double),
    ]

lib.config_area.argtypes = [ctypes.POINTER(Config)]
lib.config_area.restype = ctypes.c_int
lib.config_describe.argtypes = [ctypes.POINTER(Config)]
lib.config_describe.restype = ctypes.c_char_p

print("== 1. matching mirror ==")
print("sizeof        : python", ctypes.sizeof(Config), "| c", lib.config_sizeof())
print("offset(scale) : python", Config.scale.offset, "| c", lib.config_offset_scale())
assert ctypes.sizeof(Config) == lib.config_sizeof()
assert Config.scale.offset == lib.config_offset_scale()

cfg = Config(width=1920, height=1080, scale=1.5)
print("area          :", lib.config_area(ctypes.byref(cfg)))
print("describe      :", lib.config_describe(ctypes.byref(cfg)).decode())

# ---- 2. the same fields, declared in a different order -------------------
# A second handle, as if a different program had written its own mirror.

class ConfigWrong(ctypes.Structure):
    _fields_ = [
        ("scale", ctypes.c_double),
        ("width", ctypes.c_int),
        ("height", ctypes.c_int),
    ]

other = ctypes.CDLL("./libconfig.so")
other.config_area.argtypes = [ctypes.POINTER(ConfigWrong)]
other.config_area.restype = ctypes.c_int
other.config_describe.argtypes = [ctypes.POINTER(ConfigWrong)]
other.config_describe.restype = ctypes.c_char_p
other.config_offset_scale.restype = ctypes.c_size_t

print("\n== 2. reordered mirror ==")
print("sizeof        : python", ctypes.sizeof(ConfigWrong), "| c", lib.config_sizeof(),
      "-> agrees, and proves nothing")
print("offset(scale) : python", ConfigWrong.scale.offset, "| c",
      other.config_offset_scale(), "-> the actual disagreement")

bad = ConfigWrong(scale=1.5, width=1920, height=1080)
print("area          :", other.config_area(ctypes.byref(bad)))
print("describe      :", other.config_describe(ctypes.byref(bad)).decode())
print("no exception, no warning, no crash")

# ---- 3. a function called without declaring its return type --------------

print("\n== 3. missing restype ==")
lib.config_scaled_area.argtypes = [ctypes.POINTER(Config)]
print("default (int) :", lib.config_scaled_area(ctypes.byref(cfg)))

lib.config_scaled_area.restype = ctypes.c_double
print("c_double      :", lib.config_scaled_area(ctypes.byref(cfg)))
print("expected      :", 1920 * 1080 * 1.5)

No comments:

Post a Comment