% cross-language-ffi.tex — bridging languages through the C ABI: the strategy matrix,
% extern "C" + name mangling, static vs dynamic libraries and what the linker/loader does,
% Go c-archive/c-shared + //export, cgo, Swift @c / @_cdecl / module maps, Python ctypes,
% Ruby FFI, memory ownership across the boundary.
% Source: docs/old-notes/programming/bridging.md
% Checked 2026-09-25 against: pkg.go.dev/cmd/cgo (//export preamble rule, C.CString/C.free,
% pointer-passing rules, #cgo LDFLAGS), go help buildmode (c-archive/c-shared text),
% docs.python.org ctypes (str -> wchar_t*, default restype int, CDLL releases the GIL,
% keep CFUNCTYPE alive), github.com/ffi/ffi wiki (attach_function needs a return type),
% swift.org/blog/swift-6.3-released (@c, @c(Name), @c @implementation), ld(1) (.so searched
% before .a; archives searched once, in command-line order).
% Source errors fixed here: the Swift "dynamic" build produces libpoint.dylib/.so, not
% libpoint.a (that is -static); the C++ header lost its ';' and its extern "C" must be
% guarded by #ifdef __cplusplus to be includable from C/cgo; Python 3 must pass bytes (a str
% goes as wchar_t*); Ruby attach_function needs the return type; "linked statically" with
% -lexample is only true when no libexample.so/.dylib sits beside the .a; Go needs func main
% because the build mode requires package main — not "for the GC routines".
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/cs/cross-language-ffi.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=cs kind=concept level=senior platform=general new=no round=website-notes-2026-09-25 topic=language,build,memory
% @tags: ffi, c-abi, extern-c, name-mangling, static-library, shared-library, dlopen, cgo, go-buildmode, ctypes, ruby-ffi, cdecl
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{GoSheet}{
  morekeywords={package,import,func,defer,var,const,return,type,struct,if,else,for},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]"}
\lstdefinestyle{ffi}{style=printup,
  literate={==}{{\hbox{=}\hbox{=}}}2 {->}{{\hbox{-}\hbox{>}}}2 {::}{{\hbox{:}\hbox{:}}}2}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  ex/.style={sb, draw=sheetBrown, fill=sheetBrown!10, text width=31mm},
  co/.style={sb, draw=sheetGreen!70!black, fill=sheetGreen!10, text width=33mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
  sym/.style={font=\ttfamily\scriptsize, anchor=west, inner sep=1pt},
}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}

\begin{document}

\sheettitle{Cross-language FFI — everything meets at the C ABI}{cs · memo}

\oneliner{Languages call each other through the \textbf{C ABI}: an unmangled symbol, the
platform's C calling convention, C types (scalars, pointers, plain structs). One side
\textbf{exports} C-looking functions (\texttt{extern "C"}, Go \texttt{//export}, Swift
\texttt{@c}); the other \textbf{links} them (\texttt{-l}, cgo, module map) or
\textbf{\texttt{dlopen}s} them and declares the types (ctypes, Ruby FFI). Every allocation
keeps \emph{one} owner.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  % ── 1 the hub ──
  \node[pt] at (-0.1,3.05) {\textcolor{sheetBlue}{1} exporters $\to$ C ABI $\to$ consumers};
  \node[ex] (e1) at (1.55,2.35) {C++ \texttt{extern "C" \{ \ldots\ \}}};
  \node[ex] (e2) at (1.55,1.5) {Go \texttt{//export F} +\\\texttt{-buildmode=c-archive|c-shared}};
  \node[ex] (e3) at (1.55,0.55) {Swift \texttt{@c} (6.3) ·\\\texttt{@\_cdecl("f")} before};
  \node[sb, draw=sheetBlue, very thick, fill=sheetBlue!12, text width=17mm, minimum height=20mm] (hub) at (5.0,1.45)
    {\textbf{C ABI}\\[1pt]{\tiny symbol name\\calling conv.\\C types\\who frees}};
  \node[co] (c1) at (8.45,2.35) {C/C++, Go cgo: \texttt{\#include} +\\\texttt{-lname} — \textbf{link time}};
  \node[co] (c2) at (8.45,1.45) {Swift: module map /\\bridging header, \texttt{import}};
  \node[co] (c3) at (8.45,0.5) {Python ctypes/cffi, Ruby FFI:\\\texttt{dlopen} + declared types — \textbf{run time}};
  \foreach \e in {e1,e2,e3} \draw[flow] (\e.east) -- (hub.west);
  \foreach \c in {c1,c2,c3} \draw[hot] (hub.east) -- (\c.west);
  \node[lbl, text=sheetRed, anchor=west] at (-0.1,-0.1) {does not cross: classes, exceptions/panics, generics, GC'd objects, Swift/Go strings};
  % ── 2 mangling ──
  \draw[sheetGrey!40] (10.45,3.15) -- (10.45,-0.3);
  \node[pt] at (10.5,3.05) {\textcolor{sheetBlue}{2} why \texttt{extern "C"}};
  \node[sym] at (10.55,2.55) {void PrintHello(const char *u);};
  \node[lbl, anchor=west] at (10.55,2.15) {compiled as C++ (Itanium ABI) the symbol is};
  \node[sym, text=sheetRed] at (10.55,1.8) {\_Z10PrintHelloPKc};
  \node[lbl, anchor=west, text=sheetBrown] at (10.55,1.45) {\texttt{\_Z} · \texttt{10}+name · \texttt{P K c} = ptr to const char};
  \node[lbl, anchor=west] at (10.55,1.05) {— overloads need it; C, cgo, \texttt{dlsym} can't guess it.};
  \node[lbl, anchor=west] at (10.55,0.65) {inside \texttt{extern "C"}: C linkage, plain name};
  \node[sym, text=sheetGreen!50!black] at (10.55,0.3) {PrintHello \normalfont\tiny(Mach-O adds \texttt{\_}: \texttt{\_PrintHello})};
  \node[lbl, anchor=west, text=sheetGrey] at (10.55,-0.1) {see it: \texttt{nm libexample.a} · demangle: \texttt{c++filt}};
\end{tikzpicture}

\begin{multicols}{2}

\section{Pick a strategy}
{\scriptsize\setlength{\tabcolsep}{2.5pt}
\begin{tabular}{@{}L{21mm}L{52mm}@{}}
\toprule
host · library & strategy (e.g.) \\ \midrule
compiled · compiled & link a library via the C ABI (Go $\to$ C++, Swift $\to$ C) \\
compiled · interpreted & embed the interpreter, run scripts (CPython C API, Lua) \\
interpreted · compiled & FFI or a native extension (ctypes, cffi, Ruby FFI) \\
interpreted · interpreted & 2 processes + IPC (pipes, sockets, gRPC) or one C lib \\
\bottomrule
\end{tabular}}

\section{How it works — libraries and the linker}
\begin{itemize}\raggedright
  \item \textbf{Static} \texttt{.a} = archive of \texttt{.o}; the linker copies in only objects
        that resolve undefined symbols, searching each archive \textbf{once, in command-line
        order} (\texttt{-lfoo} goes \emph{after} its users).
  \item \textbf{Dynamic} \texttt{.so}/\texttt{.dylib}/\texttt{.dll}: the loader maps + binds it
        at launch; \texttt{dlopen}+\texttt{dlsym} at run time = what ctypes/FFI do.
  \item \texttt{ar rcs libexample.a ex.o} · \texttt{clang++ -shared -fPIC} (macOS
        \texttt{-dynamiclib}). Both in one dir: GNU ld takes the \texttt{.so}.
\end{itemize}

\section{Example — one header for C, C++ and cgo}
\begin{lstlisting}[language=C, style=ffi]
#pragma once
#ifdef __cplusplus
extern "C" {        // C++ sees C linkage; C never sees this
#endif
void  PrintHello(const char *u);      // the ';' the note lost
char *MakeGreeting(const char *name); // malloc'd ...
void  FreeGreeting(char *p);          // ... freed HERE
#ifdef __cplusplus
}
#endif
\end{lstlisting}
\begin{lstlisting}[language=GoSheet, style=ffi]
// #cgo LDFLAGS: -L${SRCDIR} -lexample
// #include <stdlib.h>
// #include "example.h"
import "C"       // the comment right above is the preamble
import "unsafe"
func main() {
    s := C.CString("Hi")             // malloc'd C copy
    defer C.free(unsafe.Pointer(s))  // Go frees it
    C.PrintHello(s) }
\end{lstlisting}

\section{Go as the library}
\begin{lstlisting}[language=GoSheet, style=ffi]
//export SayHello
func SayHello(s *C.char) { fmt.Println(C.GoString(s)) }
func main() {}  // package main required; never called
// go build -buildmode=c-shared -o libhello.so  (c-archive: .a)
\end{lstlisting}
Only \texttt{//export}ed functions are callable; the Go runtime (GC, scheduler) ships inside;
a C header is generated beside it\unverified. With \texttt{//export}, the preamble may only
\emph{declare} (it is copied into two C files).

\columnbreak

\section{Python and Ruby — load at run time}
\begin{lstlisting}[language=Python, style=ffi]
lib = CDLL("./libexample.so")         # dlopen
lib.PrintHello.argtypes = [c_char_p]
lib.PrintHello(b"Hi")     # bytes -> char*;  a str -> wchar_t*
lib.MakeGreeting.restype = c_void_p   # default int truncates
p = lib.MakeGreeting(b"Ann"); print(c_char_p(p).value) # copy
lib.FreeGreeting(c_void_p(p))         # the lib's own free
\end{lstlisting}
\begin{lstlisting}[language=Ruby, style=ffi]
module Ex; extend FFI::Library; ffi_lib './libexample.so'
  attach_function :PrintHello, [:string], :void # ret: required
end;  Ex.PrintHello("Hi")
\end{lstlisting}
\texttt{CDLL} \textbf{releases the GIL} for the call; cffi parses C headers.

\section{Swift at the border}
\begin{itemize}\raggedright
  \item \textbf{In}: module map (\texttt{header "example.h" link "example"}).
        \textbf{Out}: \texttt{@c} / \texttt{@c(MyLib\_f)} (6.3) $\to$ generated header;
        \texttt{@c @implementation} implements a C-declared fn; earlier \texttt{@\_cdecl}.
  \item Swift$\to$Swift is \textbf{not} FFI: \texttt{-emit-library} = \texttt{.dylib}/\texttt{.so}
        (\texttt{-static}: \texttt{.a}) + \texttt{.swiftmodule} — same compiler only.
\end{itemize}

\section{Memory ownership}
\begin{itemize}\raggedright
  \item \textbf{Whoever allocates, frees} (the lib exports \texttt{FreeGreeting}).
  \item \textbf{Go}: \texttt{C.CString} mallocs (you \texttt{C.free}); C keeps no Go pointer
        past the call unless pinned (\texttt{runtime.Pinner}).
  \item \textbf{GC'd side}: keep ctypes \texttt{CFUNCTYPE} callbacks referenced (else GC
        $\to$ crash); a \texttt{withCString} pointer dies with its closure. C++ exceptions:
        catch in the wrapper\unverified.
\end{itemize}

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{No \texttt{extern "C"} $\to$ \emph{undefined reference to \texttt{PrintHello}}; an
        unguarded \texttt{extern "C"} in a header breaks C/cgo.}
  \trap{ctypes: no \texttt{restype} truncates a 64-bit pointer to \texttt{int}; a \texttt{str}
        arrives as \texttt{wchar\_t*} — C prints ``H''.}
  \trap{``Statically linked'' with a \texttt{.so}/\texttt{.dylib} beside the \texttt{.a}: links,
        then fails to launch elsewhere (\texttt{-Bstatic}).}
\end{itemize}

\section{Remember}
\textbf{Speak C at the border: plain names, C types, one owner.}

\section{Likely questions}
\begin{enumerate}\raggedright
  \item Why C? — stable platform ABI, no mangling, no runtime.
  \item \texttt{extern "C"}? — plain symbol, C convention, no overloads.
  \item Static vs dynamic? — copied at link vs mapped by the loader.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} swift-c-cpp-interop · objc-interop ·
linking-and-launch-time · c-undefined-behavior-and-build (linkage, archives) · node-runtime
(\texttt{child\_process} as IPC)}

\end{document}
