Workspace IndexAlgorithms › Day 32

FFI/ABI Boundaries and Safety (Panics, Alignment, Lifetimes) TODO

Algorithms · Day 32 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

The ABI is the binary-level contract that compiled code must honor — it specifies the calling convention (which registers pass arguments, stack alignment, where return values go), struct layout and padding, name mangling, and more. FFI is the mechanism by which different languages call each other through that ABI, and since the C ABI is usually the common denominator, structs typically need an explicit C layout. Safety problems arise along three axes. First, if a panic or exception unwinds across an FFI boundary, the other language's runtime can't handle it, so it becomes undefined behavior — it must be caught at the boundary and converted into an error code. Second, if alignment or size assumptions are wrong, you get invalid memory accesses. Third, if pointer ownership and lifetime cross the language boundary, memory a GC has moved or reclaimed can be left dangling for the other side to reference — so the contract must nail down exactly who allocates and who frees.

When native libraries for cryptography or a DB engine are wired in, most crashes trace back not to logic but to this boundary contract — leaked panics, GC-moved memory, unclear ownership of freeing.

Code & Formula

# FFI·ABI 경계와 안전성 — ctypes 로 구조체 레이아웃(패딩)을 확인하고, 패닉이 경계를 넘지 못하도록 에러 코드로 변환한다.

import ctypes

class Header(ctypes.Structure):
    # C ABI 기준: int8 뒤에 int32 가 오면 정렬(4바이트) 때문에 3바이트 패딩이 끼어든다.
    _fields_ = [("flag", ctypes.c_int8), ("value", ctypes.c_int32)]

h = Header(flag=1, value=1000)
print("sizeof(Header):", ctypes.sizeof(h), "bytes  (1 + 3 padding + 4, not 5)")
print("offsetof(value):", Header.value.offset, "  <- 정렬 때문에 1이 아니라 4")

# FFI 경계에서는 상대 언어 런타임이 이해 못 하는 예외/패닉이 넘어가면 정의되지 않은 동작이 된다.
# 규칙: 경계 함수는 절대 예외를 던지지 않고, 항상 (ok, error_code) 형태로 변환해 반환한다.
ERR_OK = 0
ERR_DIVIDE_BY_ZERO = 1
ERR_OUT_OF_RANGE = 2

def ffi_safe_divide(a, b):
    """다른 언어에서 호출한다고 가정한 경계 함수 — 내부 예외를 절대 누출시키지 않는다."""
    try:
        return (ERR_OK, a / b)
    except ZeroDivisionError:
        return (ERR_DIVIDE_BY_ZERO, None)   # 예외 대신 에러 코드로 변환해서 경계를 넘긴다
    except OverflowError:
        return (ERR_OUT_OF_RANGE, None)

ok1, r1 = ffi_safe_divide(10, 2)
ok2, r2 = ffi_safe_divide(10, 0)

print("divide(10, 2) ->", "code", ok1, "result", r1)
print("divide(10, 0) ->", "code", ok2, "result", r2, " (호출자는 예외가 아니라 코드로 실패를 본다)")

Exercise

Build a minimal example that calls a C function from Go via cgo, write code that passes a Go slice pointer into C, stores it, and uses it later, then check against the documentation to see which rule it violates.

Practical Connection

Ethereum clients often delegate signature and pairing operations like secp256k1 or BLS to C/assembly libraries, so lifetime and panic handling at the FFI boundary directly affect node stability.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

FFI·ABI 경계와 안전성(패닉·정렬·수명) TODO

Algorithms · Day 32 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

개념

ABI는 컴파일된 코드끼리 지켜야 하는 이진 수준 계약으로 호출 규약(인자 전달 레지스터, 스택 정렬, 반환값 위치), 구조체 레이아웃과 패딩, 이름 맹글링 등을 규정한다. FFI는 서로 다른 언어가 이 ABI를 매개로 호출하는 방식이며, 보통 C ABI를 공통분모로 삼기 때문에 구조체에 명시적 C 레이아웃 지정이 필요하다. 안전성 문제는 세 축에서 생긴다. 첫째, 패닉이나 예외가 FFI 경계를 넘어 되감기(unwind)하면 상대 언어의 런타임이 이를 처리할 수 없어 정의되지 않은 동작이 되므로 경계에서 잡아 에러 코드로 변환해야 한다. 둘째, 정렬과 크기 가정이 어긋나면 잘못된 메모리 접근이 되고, 셋째, 포인터의 소유권과 수명이 언어 경계를 넘으면 GC가 이동/회수한 메모리를 상대가 참조하는 dangling이 생기므로 누가 할당하고 누가 해제하는지를 규약으로 못 박아야 한다.

암호 라이브러리나 DB 엔진을 네이티브로 붙일 때 대부분의 크래시는 로직이 아니라 이 경계 규약(패닉 누출, GC 이동, 해제 책임 불명확)에서 나온다.

코드 · 수식

# FFI·ABI 경계와 안전성 — ctypes 로 구조체 레이아웃(패딩)을 확인하고, 패닉이 경계를 넘지 못하도록 에러 코드로 변환한다.

import ctypes

class Header(ctypes.Structure):
    # C ABI 기준: int8 뒤에 int32 가 오면 정렬(4바이트) 때문에 3바이트 패딩이 끼어든다.
    _fields_ = [("flag", ctypes.c_int8), ("value", ctypes.c_int32)]

h = Header(flag=1, value=1000)
print("sizeof(Header):", ctypes.sizeof(h), "bytes  (1 + 3 padding + 4, not 5)")
print("offsetof(value):", Header.value.offset, "  <- 정렬 때문에 1이 아니라 4")

# FFI 경계에서는 상대 언어 런타임이 이해 못 하는 예외/패닉이 넘어가면 정의되지 않은 동작이 된다.
# 규칙: 경계 함수는 절대 예외를 던지지 않고, 항상 (ok, error_code) 형태로 변환해 반환한다.
ERR_OK = 0
ERR_DIVIDE_BY_ZERO = 1
ERR_OUT_OF_RANGE = 2

def ffi_safe_divide(a, b):
    """다른 언어에서 호출한다고 가정한 경계 함수 — 내부 예외를 절대 누출시키지 않는다."""
    try:
        return (ERR_OK, a / b)
    except ZeroDivisionError:
        return (ERR_DIVIDE_BY_ZERO, None)   # 예외 대신 에러 코드로 변환해서 경계를 넘긴다
    except OverflowError:
        return (ERR_OUT_OF_RANGE, None)

ok1, r1 = ffi_safe_divide(10, 2)
ok2, r2 = ffi_safe_divide(10, 0)

print("divide(10, 2) ->", "code", ok1, "result", r1)
print("divide(10, 0) ->", "code", ok2, "result", r2, " (호출자는 예외가 아니라 코드로 실패를 본다)")

연습

Go에서 cgo로 C 함수를 호출하는 최소 예제를 만들고, Go 슬라이스 포인터를 C에 넘겨 저장했다가 나중에 쓰는 코드를 작성해 어떤 규칙을 위반하는지 문서와 대조해 볼 것.

실무 · Verex 연결

이더리움 클라이언트는 secp256k1이나 BLS 같은 서명·페어링 연산을 C/어셈블리 라이브러리로 위임하는 경우가 많아, FFI 경계의 수명과 패닉 처리가 노드 안정성에 직결된다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 31. 메모리 할당자 설계33. 결정론적 실행 →