Published on

Cpp Learning (1): Prelude

Authors
  • avatar
    Name
    Xiaolong Peng
    Twitter

中文版: 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

C++ systems stack overview

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?

ReasonExplanation
C ABI is stableLowest common denominator across compilers and platforms
C++ features are not portablestd::function, lambdas, member functions, overloads, and name mangling differ across toolchains
Foreign runtimes lack C++ supportJava/Kotlin/JS/Python cannot construct or destroy C++ objects directly
FFI and the C ABI bridge

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
Opaque handle lifecycle

Why not expose C++ object pointers directly?

  1. C cannot invoke constructors or destructors
  2. Memory layout (vtable, member order) is not standardized
  3. GC or object relocation may invalidate pointers (e.g. Kotlin/Native ARC)

3. C++ Type System and ABI Compatibility

POD vs non-POD

TypeCharacteristicsCross-ABI usage
POD / standard layoutC-compatible memory layoutSafe to pass by value
Non-PODVirtual functions, non-trivial ctor/dtor, reference members, etc.Must use opaque handles
POD vs handle decision tree

Calling conventions

ConventionPlatformStack cleanupTypical use
__cdeclWindowsCallerDefault C
__stdcallWindowsCalleeWin32 API
__thiscallWindowsCallee (this in ECX)C++ member functions
System V AMD64Linux / macOSCaller (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 resource lifecycle

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

FeatureFlagReason
Exceptions-fno-exceptionsSize, determinism, no unwind tables
RTTI-fno-rttiSave space; disables dynamic_cast
Global constructors-nostdlib + manual initAvoid CRT dependency

6. Debugger Internals

Software breakpoints

When execution reaches a target address, the debugger has replaced the first byte with 0xCC (INT 3):

  1. 0xCC executes → #BP exception → kernel → SIGTRAP on Linux
  2. On hit: restore original opcode → single-step → re-insert 0xCC
Software breakpoint flow

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

LLDB object destruction flow
  • RAII + shared_ptr for 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 and embedded 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

ConceptRoleEmbedded choice
CRT initHeap, stdio, global ctor setup before mainDisable (-nostdlib), init manually
RTTItypeid, dynamic_castDisable (-fno-rtti) to save space
Exception unwindStack unwinding and cleanupDisable (-fno-exceptions)

9. Concept Map: Where These Ideas Lead

Topic in this postNext stepOne-liner
C ABIItanium C++ ABIIndustrial standard for mangling and vtables
Opaque handlesCOM IUnknown, OpenSSL EVP_*Decades-old stable cross-module encapsulation
POD / layoutstd::is_standard_layoutStatic checks since C++11
RAIIScope guard / deferSame idea as Go's defer
ptraceperf, strace, eBPFSame family of execution observability
Software breakpointsHardware breakpoints DR0–DR3Limited slots for critical addresses
Cross-compilationCMake toolchain filesStandard modern workflow
Node-APIN-API docsStable ABI for Node native addons
JNIJNI specStandard JVM ↔ native bridge

10. Further Reading

Official docs and specs

TitleFocus
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 LoadersABI, symbol resolution, dynamic linking
21st Century CModern C practice compared with C++
Computer Systems: A Programmer's PerspectiveHow programs actually run on machines
Debugging with GDBDebugger 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)-引子