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.
| Acronym | Level | A 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
- Two contracts — the distinction, and why it costs you a rebuild
- Struct layout — the part you can print out
- Symbols and registers — what the linker sees, and where arguments go
- Crossing from Python — where a mismatch stops being theoretical
- Keeping an ABI — what breaks what, and how libraries survive it
- 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:
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.
| Change | API | ABI | What users must do |
|---|---|---|---|
| Fix a bug inside a function body | intact | intact | Nothing. Drop in the new .so. |
| Add a brand-new function | intact | intact | Nothing, unless they want the new one. |
| Rename a function | broken | broken | Edit their source, rebuild. The compiler tells them. |
| Reorder two struct fields | intact | broken | Rebuild — and nothing tells them. |
Change a field from int to long |
mostly intact | broken | 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:
- Struct layout — which byte each field starts at, and how big the whole thing is. Part 2.
- Symbol naming — what the function is called in the object file. Part 3.
- The calling convention — which register or stack slot each argument travels in, and who cleans up. Part 3.
- 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.
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.
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))
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.
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.
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.
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; }
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:
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:
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.
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; }
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.
| Kind | Arguments 1–6 | Beyond that | Return |
|---|---|---|---|
| Integers and pointers | rdi rsi rdx rcx r8 r9 |
pushed on the stack | rax |
| Floats and doubles | xmm0 … xmm7 |
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.
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.
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.
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()
== 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.
class ConfigWrong(ctypes.Structure):
_fields_ = [
("scale", ctypes.c_double),
("width", ctypes.c_int),
("height", ctypes.c_int),
]
== 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.
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.
== 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
sizeofproves very little. Compare offsets, field by field, or compare nothing. - Have the library describe itself. Two exported functions turned an invisible failure into an
assertthat 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 function | API | ABI |
|---|---|---|
| Add a function | ok | ok |
| Change a function body | ok | ok |
| Rename a parameter in the header | ok | ok |
| Reorder struct fields | ok | broken |
| Insert a field in the middle | ok | broken |
Widen a field, int → long | ok | broken |
| Add a field at the end | ok | broken if callers allocate it |
| Change a parameter's type | broken | broken |
| Remove or rename a function | broken | broken |
Change a #define constant | behaviour changes | ok |
"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.
/* 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.
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.
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
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.so | Exported symbols — the callable ABI surface |
nm -C, c++filt | Demangle C++ symbol names |
readelf -d lib.so | soname and the list of libraries it needs |
readelf -Ws lib.so | Full symbol table with sizes and visibility |
objdump -d --no-show-raw-insn f.o | Disassembly — see the calling convention directly |
ldd ./program | Which libraries actually get loaded, and from where |
gcc -fdump-lang-raw, pahole | Struct layout and padding, reported by the toolchain |
offsetof, _Alignof, _Static_assert | Assert the layout from inside the program itself |
Cheat sheet
The distinction
| API | Source-level contract. Broken → compiler error, with a line number. |
| ABI | Binary-level contract. Broken → wrong answers, usually with no message at all. |
x86-64 System V, in four lines
| Integer / pointer arguments | rdi rsi rdx rcx r8 r9, then the stack |
| Float / double arguments | xmm0–xmm7, counted separately |
| Return value | rax, or xmm0 for floating point |
| Struct field offsets | Each field aligned to its own width; total rounded up to the largest |
Writing an FFI binding
Set argtypes and restype | On every single function. The defaults are wrong for pointers and doubles. |
Assert sizeof and offsets | Sizes can match while layouts differ. |
| Export a self-description | Two functions returning sizeof and an offsetof pay for themselves. |
| Prefer opaque pointers | A 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
sizeofand 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
- The x86-64 psABI — the actual specification; chapter 3 is the calling convention and the struct classification rules
- ctypes — the standard-library FFI used throughout part 4
- Program Library HOWTO — sonames, versioning and the symlink convention
- How to Write Shared Libraries — Ulrich Drepper; dense, and still the best single document on symbol visibility and versioning
- Language linkage — what
extern "C"is defined to do
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.
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
#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
/* 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
/* 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
#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
"""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