- Published on
Cpp Learning (1): Prelude
- Authors
- Name
- Xiaolong Peng
中文版: Cpp学习(1)-引子
Prelude
A discussion spanning Kotlin/Native, JNI, Node-API, compiler runtimes, and LLDB eventually converges on one question:
When C++ must talk to the outside world, where is the stable boundary?
The answer is almost always the C ABI—the lowest common denominator that is stable across compilers, platforms, and languages. This post organizes that discussion into a systems-level map of C++ fundamentals.
1. A Full-Stack View of C++ Systems
How to read the diagram (top to bottom):
- Application layer: cross-language FFI bindings (Kotlin/Native, JNI, Node-API, etc.)
- C ABI stability layer: every cross-language call must pass through here—
extern "C", opaque handles, POD structs - C++ implementation layer: RAII, virtual functions, and templates are free to use here, but must stay hidden externally
- Runtime & OS services: ptrace, signals, CRT, breakpoint opcode
0xCC, calling conventions - Hardware / OS layer: execution model and low-level system resource abstractions
2. Cross-Language FFI and the C ABI
Why must FFI use the C calling convention?
| Reason | Explanation |
|---|---|
| C ABI is stable | Lowest common denominator across compilers and platforms |
| C++ features are not portable | std::function, lambdas, member functions, overloads, and name mangling differ across toolchains |
| Foreign runtimes lack C++ support | Java/Kotlin/JS/Python cannot construct or destroy C++ objects directly |
Opaque handles
// C header — expose only a pointer type, never the layout
typedef struct MyState* MyHandle;
MyHandle create(void);
void destroy(MyHandle h);
void do_work(MyHandle h, int arg);
- A handle may be
void*or a forward-declared struct pointer - All operations go through C API functions, which internally dispatch to C++ objects
- Used to wrap stateful C++ objects while hiding their layout
Why not expose C++ object pointers directly?
- C cannot invoke constructors or destructors
- Memory layout (vtable, member order) is not standardized
- GC or object relocation may invalidate pointers (e.g. Kotlin/Native ARC)
3. C++ Type System and ABI Compatibility
POD vs non-POD
| Type | Characteristics | Cross-ABI usage |
|---|---|---|
| POD / standard layout | C-compatible memory layout | Safe to pass by value |
| Non-POD | Virtual functions, non-trivial ctor/dtor, reference members, etc. | Must use opaque handles |
Calling conventions
| Convention | Platform | Stack cleanup | Typical use |
|---|---|---|---|
__cdecl | Windows | Caller | Default C |
__stdcall | Windows | Callee | Win32 API |
__thiscall | Windows | Callee (this in ECX) | C++ member functions |
| System V AMD64 | Linux / macOS | Caller (registers preferred) | Unified convention |
On Linux / macOS you usually do not need explicit keywords—the platform ABI is fixed by specification.
4. C ABI Implementation Example
Header myapi.h:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct MyHandle* MyHandle;
MyHandle my_create(void);
void my_destroy(MyHandle h);
int my_do(MyHandle h, int x);
#ifdef __cplusplus
}
#endif
Implementation myapi.cpp:
#include "myapi.h"
class MyImpl {
public:
int work(int x) { return x * 2; }
};
struct MyHandle {
MyImpl impl;
};
extern "C" {
MyHandle my_create() { return new MyHandle; }
void my_destroy(MyHandle h) { delete h; }
int my_do(MyHandle h, int x) { return h->impl.work(x); }
}
This is the pattern behind Node-API, JNI, pybind11, and many other bindings: C faces upward, C++ faces inward.
5. Resource Management and RAII
Core idea
Resource Acquisition Is Initialization—acquire in the constructor, release in the destructor.
RAII still works under cross-compilation as long as the target C++ runtime matches the toolchain.
RAII wrapper for Windows HANDLE
struct HandleDeleter {
void operator()(HANDLE h) const {
if (h) CloseHandle(h);
}
};
using UniqueHandle = std::unique_ptr<void, HandleDeleter>;
Features often disabled in embedded builds
| Feature | Flag | Reason |
|---|---|---|
| Exceptions | -fno-exceptions | Size, determinism, no unwind tables |
| RTTI | -fno-rtti | Save space; disables dynamic_cast |
| Global constructors | -nostdlib + manual init | Avoid CRT dependency |
6. Debugger Internals
Software breakpoints
When execution reaches a target address, the debugger has replaced the first byte with 0xCC (INT 3):
0xCCexecutes →#BPexception → kernel → SIGTRAP on Linux- On hit: restore original opcode → single-step → re-insert
0xCC
The ptrace system call
ptrace is for process tracing—it is not a signal. Common capabilities:
- Attach / detach target processes
- Read / write memory and registers
- Single-step and continue execution
LLDB and GDB rely heavily on ptrace on Linux for breakpoints, memory access, and thread control.
Destroy semantics in LLDB
- RAII +
shared_ptrfor automatic lifetime management - Explicit
Destroy()for platform-specific cleanup (e.g.ptrace(PTRACE_KILL)) - Observer pattern to broadcast destruction and avoid dangling references
7. Cross-Compilation and "Local C"
Cross-compilation
Build executables for one platform (ARM bare-metal, RTOS) from another (x86_64 host).
What is "local C"?
Use C-style syntax and libraries (printf, malloc) inside .cpp files, while still benefiting from C++ type checking and namespace organization.
Common in bare-metal / RTOS projects with:
- No global constructors
- No exceptions or RTTI
- Manual CRT / heap initialization
8. Runtime Basics: CRT and RTTI
| Concept | Role | Embedded choice |
|---|---|---|
| CRT init | Heap, stdio, global ctor setup before main | Disable (-nostdlib), init manually |
| RTTI | typeid, dynamic_cast | Disable (-fno-rtti) to save space |
| Exception unwind | Stack unwinding and cleanup | Disable (-fno-exceptions) |
9. Concept Map: Where These Ideas Lead
| Topic in this post | Next step | One-liner |
|---|---|---|
| C ABI | Itanium C++ ABI | Industrial standard for mangling and vtables |
| Opaque handles | COM IUnknown, OpenSSL EVP_* | Decades-old stable cross-module encapsulation |
| POD / layout | std::is_standard_layout | Static checks since C++11 |
| RAII | Scope guard / defer | Same idea as Go's defer |
| ptrace | perf, strace, eBPF | Same family of execution observability |
| Software breakpoints | Hardware breakpoints DR0–DR3 | Limited slots for critical addresses |
| Cross-compilation | CMake toolchain files | Standard modern workflow |
| Node-API | N-API docs | Stable ABI for Node native addons |
| JNI | JNI spec | Standard JVM ↔ native bridge |
10. Further Reading
Official docs and specs
Recommended books
| Title | Focus |
|---|---|
| The C++ Programming Language (4th Ed.) | Full C++ feature set, especially the C subset |
| Effective Modern C++ | Modern best practices; C vs C++ boundaries |
| Linkers and Loaders | ABI, symbol resolution, dynamic linking |
| 21st Century C | Modern C practice compared with C++ |
| Computer Systems: A Programmer's Perspective | How programs actually run on machines |
| Debugging with GDB | Debugger mechanics (concepts transfer to LLDB) |
Topics for a future Part 2
- Dynamic linking: PLT/GOT, lazy binding,
dlopen, plugin architectures - Memory model:
memory_order, lock-free code, thread safety at FFI boundaries - Sanitizers: how ASan / TSan / UBSan cooperate with debuggers
- WebAssembly / Emscripten: yet another platform where C ABI is the boundary
Summary
The central tension in C++ systems programming:
Express internally with C++; stabilize externally with the C ABI.
Opaque handles are the bridge. RAII is the discipline. POD is the contract. ptrace is the observation window. Once you internalize this map, designs in Kotlin/Native, Node-API, or LLDB stop looking like magic—they are the same boundary strategy applied in different contexts.
Part 1 of the "Cpp Learning" series, distilled from a cross-language systems discussion.
中文版: Cpp学习(1)-引子