Skip to content

Instantly share code, notes, and snippets.

@planetis-m
Last active February 2, 2026 17:56
Show Gist options
  • Select an option

  • Save planetis-m/7947f81434f46ab7796d66b93b4616cd to your computer and use it in GitHub Desktop.

Select an option

Save planetis-m/7947f81434f46ab7796d66b93b4616cd to your computer and use it in GitHub Desktop.

You are writing a standalone guide for an AI coding agent that will manually wrap arbitrary C libraries to Nim. The reader cannot see this repository and must not need it. Your job is to produce a self-contained SKILLS.md that teaches patterns, idioms, pitfalls, and provides examples.

Deliverable

Create/overwrite a single file: SKILLS.md (Markdown).

Hard constraints

  • No repository references: do not mention naylib, raylib, file paths, tools, configs, snippets, “evidence”, or grep instructions.
  • Manual wrapping only: assume the reader has only C headers/docs and Nim. No code generator assumptions.
  • Patterns and examples only: the guide must teach reusable idioms with generic examples (LIB_*, foo_*) and must be applicable to any C library.
  • Follow idiomatic Nim style (stdlib style): naming, casing, module layout conventions.

What SKILLS.md must contain (required sections)

  1. Purpose

    • What a C→Nim wrapper is, goals (ABI correctness + Nim ergonomics).
    • The recommended two-layer design:
      • Raw FFI layer: ABI-faithful bindings
      • Ergonomic Nim layer: safer, nicer API
  2. Project Layout & Module Strategy

    • How to split a large C library into Nim modules.
    • Public vs private symbols, re-exports, and keeping raw bindings separate.
  3. Naming & API Conventions (Idiomatic Nim)

    • Prefix stripping (remove LIB_, foo_), avoiding collisions.
    • Casing rules:
      • Types/enums/objects: PascalCase
      • Procs/vars: lowerCamelCase
      • Constants: idiomatic Nim (const, not #define semantics)
    • When to keep original C names (clarity/compatibility).
    • Handling keywords and reserved names.
  4. FFI Mechanics Cheatsheet

    • importc, cdecl, header, dynlib: when and how to use each.
    • Static vs dynamic linking approaches.
    • Platform/architecture conditionals.
    • “Don’t accidentally change ABI” rules.
  5. C→Nim Type Mapping (Table + Examples) Include a mapping table and examples for:

    • Integers (int, unsigned, long, size_t, intptr_t, etc.)
    • Floats (float, double)
    • char* / const char* (cstring), and safe conversions to string
    • Pointers (ptr T, nullable pointers), out-parameters (var T)
    • Arrays:
      • fixed-size arrays in structs
      • pointer+length buffers
      • ptr UncheckedArray[T] patterns
    • Opaque handles (recommended representations)
    • Structs and alignment/packing concerns
  6. Wrapping Enums, Flags, and Constants

    • Enums: stable naming, explicit values, conversion helpers
    • Bitflags: recommended Nim representation + helper procs/templates
    • Macros: when to translate into const, when to wrap as procs/templates
    • When you cannot represent a macro directly (function-like macros)
  7. Wrapping Functions

    • Writing raw proc signatures that match C exactly.
    • Common patterns:
      • “create/destroy” pairs
      • getter/setter patterns
      • in/out params
      • functions returning pointers (nullable vs owned)
    • Ergonomic layer patterns:
      • overloads accepting string and converting to cstring
      • overloads accepting openArray[T] and passing pointer/len
      • hiding out-params behind return values when safe
  8. Callbacks / Function Pointers

    • Declaring callback types (proc(...) {.cdecl.}).
    • Closure vs non-closure safety (why capturing closures is dangerous).
    • Userdata/context pointer patterns.
    • Threading and GC safety notes.
  9. Memory Ownership, Lifetime, and Safety

    • Explicit ownership rules: borrowed vs owned pointers.
    • When to copy data into Nim-managed memory.
    • Resource management patterns:
      • explicit close/free/destroy
      • optional RAII-like wrappers (only if safe)
    • Avoiding leaks and double-free.
  10. Error Handling Patterns

  • Return codes, errno, null pointers.
  • Provide both:
    • raw error codes (thin layer)
    • idiomatic wrappers (raise exceptions or return Option-like results)
  • Designing predictable, testable error behavior.
  1. Testing & Verification Checklist
  • Compile checks, link checks, minimal smoke tests.
  • ABI checks (sizeof/alignment) where feasible.
  • Common failure symptoms and debugging steps.
  1. Common Pitfalls (Must be concrete) Include at least:
  • wrong calling convention
  • wrong integer widths / size_t
  • passing Nim string directly to C
  • struct packing mismatch
  • returning pointers to temporary memory
  • callback capturing GC-managed state
  • using seq memory where C expects stable pointers
  • lifetime issues with cstring

Example requirements (mandatory)

Throughout the guide, include multiple small generic examples that show:

  • A tiny C header snippet
  • The corresponding raw Nim FFI binding
  • A safer/idiomatic Nim wrapper proc on top

Example theme suggestions you must cover:

  • strings in/out
  • pointer+length buffers
  • create/destroy resource
  • callback registration

Output rules

  • Write only SKILLS.md.
  • No references to this repo or to naylib/raylib.
  • Make it actionable: checklists, “Do/Don’t”, minimal examples.

Prompt 1 — LLM Judge: Evaluate candidate SKILLS.md files and pick the best

You are an expert judge evaluating multiple candidate SKILLS.md documents. These documents are meant to instruct an AI coding agent to manually wrap arbitrary C libraries into idiomatic Nim.

Inputs

You will be given N candidate documents: SKILLS_A.md, SKILLS_B.md, … (content pasted in full).

Non‑negotiable requirements the best guide must satisfy

The target reader:

  • must wrap C libraries manually (no generator assumptions)
  • cannot access any naylib/raylib repository files or implementation details

Therefore, the best SKILLS.md must:

  • be self-contained
  • be library-agnostic (no raylib-specific guidance)
  • avoid references to repo file paths, configs, “evidence”, grep steps, etc.
  • include actionable patterns/idioms, pitfalls, and examples
  • follow idiomatic Nim conventions (stdlib style mindset)
  • teach both ABI-correct raw bindings and ergonomic Nim wrappers

Your tasks

  1. Summarize each candidate in 4–8 bullets:

    • what it teaches well
    • what it misses
    • who it would help/hurt (beginner vs advanced agent)
  2. Score each candidate using this rubric (0–5 each; total /35):

    • Manual-wrapping focus (no generator/config framing)
    • Library-agnostic + self-contained
    • Correctness/ABI realism (calling conventions, types, ownership)
    • Idiomatic Nim API design quality
    • Practicality (checklists, step-by-step workflow)
    • Examples quality (generic C snippets + Nim raw + Nim ergonomic layer)
    • Pitfalls & debugging guidance
  3. Advantages/Disadvantages table

    • Provide a table with columns: Candidate, Strengths, Weaknesses, Risks.
  4. Best instructions extraction

    • Extract the best concrete instructions across all candidates:
      • “Top 15 rules” (short, imperative)
      • “Top 10 pitfalls + how to avoid them”
      • “Best example patterns to keep” (list the example themes)
  5. Select the winner

    • Pick the single best candidate and justify why, referencing the rubric.
  6. Provide an improvement plan

    • For the winner: list the top 10 edits that would make it excellent.
    • For the others: the top 3 salvageable parts from each.

Output format

Use this exact structure:

  1. Overall verdict
  2. Per-candidate summaries
  3. Scores (rubric)
  4. Advantages/Disadvantages table
  5. Best instructions to carry forward
  6. Winner + justification
  7. Improvement plan

Be direct and specific. Do not be polite/fluffy. Focus on what will best instruct an AI agent to implement real wrappers.


Prompt 2 — LLM Judge: Synthesize a new SKILLS.md from the best ideas

You are now the author. You have reviewed multiple candidate SKILLS.md documents and extracted the best elements. Write a brand-new SKILLS.md that is the best possible standalone guide for an AI agent manually wrapping arbitrary C libraries into Nim.

Inputs

You will be provided:

  • The candidates SKILLS_A.md, SKILLS_B.md, … (full text)
  • Optionally: your own prior “best instructions to carry forward” notes

Hard constraints

  • Self-contained: do not reference any repository, raylib/naylib, configs, generator pipelines, or file paths.
  • Manual wrapping only.
  • Library-agnostic; use only generic library names (LIB_, foo_).
  • Must include concrete examples: C header snippet → raw Nim FFI → idiomatic Nim wrapper.
  • Must teach: naming, type mapping, ABI pragmas, ownership/lifetime, callbacks, error handling, testing, and pitfalls.
  • Must follow idiomatic Nim naming/style principles.

Required structure (use these headings)

  1. Purpose
  2. Two-Layer Wrapper Architecture (Raw FFI vs Idiomatic Layer)
  3. Naming & API Design Rules (Idiomatic Nim)
  4. FFI Mechanics: Pragmas, Linking, and Portability
  5. C→Nim Type Mapping (Table + Notes)
  6. Structs, Enums, Flags, and Constants
  7. Function Binding Patterns (in/out params, buffers, strings)
  8. Callbacks and User Data
  9. Ownership, Lifetimes, and Resource Management
  10. Error Handling Strategies
  11. Testing & Verification Checklist
  12. Common Pitfalls (and fixes)
  13. Minimal Template to Start a New Wrapper (copy/paste scaffold)

Quality bar

  • Prioritize correctness and reproducibility over elegance.
  • Include “Do/Don’t” bullets.
  • Keep examples small but realistic (cover strings, buffers, resources, callbacks).
  • Provide a final concise checklist an agent can follow during implementation.

Output

Output only the final SKILLS.md content in Markdown. No commentary, no analysis, no mention of candidates.

C→Nim Manual Wrapping Skills

1. Purpose

A C→Nim wrapper exposes a C library to Nim while preserving ABI correctness and offering an idiomatic Nim API. The goals are:

  • ABI correctness: exact layouts, calling conventions, and signatures.
  • Nim ergonomics: safer, clearer, and more “Nim-like” usage.

Recommended two-layer design:

  1. Raw FFI layer: faithful bindings to C with minimal interpretation.
  2. Ergonomic Nim layer: safe, friendly wrappers that hide pitfalls and add conveniences.

Keep the raw layer stable and thin. Build the ergonomic layer on top so you can adjust usability without changing the ABI surface.


2. Project Layout & Module Strategy

Split modules by library domain and keep raw bindings isolated from idiomatic wrappers.

Suggested module pattern (conceptual):

  • lib_raw_*: raw FFI modules (structs, enums, importc procs)
  • lib_*: ergonomic modules (overloads, helpers, resource management)

Public vs private symbols:

  • Export raw symbols only if needed for advanced users.
  • Re-export selected symbols from ergonomic modules to provide a clean public API.
  • Keep internal helpers private to avoid API bloat.

When splitting a large library:

  • Mirror the C header structure (by subsystem).
  • Avoid cyclic imports by centralizing shared types in a lib_raw_types module.
  • Keep public surface small and predictable.

3. Naming & API Conventions (Idiomatic Nim)

Prefix stripping:

  • Remove common prefixes like LIB_, foo_, FOO_ when they don’t add clarity.
  • Preserve names when they disambiguate or match common documentation terminology.

Casing rules:

  • Types/enums/objects: PascalCase
  • Procs/vars: lowerCamelCase
  • Constants: const in Nim with PascalCase (or CamelCase) as appropriate

When to keep original C names:

  • Well-known API names that are part of the library’s identity.
  • Names that would collide after stripping prefixes.
  • When upstream docs refer to exact names heavily.

Reserved names / keywords:

  • Add a suffix like * in docs but in code use a consistent rename (e.g., typetyp, addraddress).
  • Consider importc: "..." to preserve the C name while using a safe Nim name.

4. FFI Mechanics Cheatsheet

Core pragmas:

  • importc: bind a Nim symbol to a C symbol.
  • cdecl: default C calling convention (use stdcall or others if C library requires).
  • header: specify header for C compilation (static linking scenarios).
  • dynlib: resolve symbols from a shared library at runtime.

Static vs dynamic linking:

  • Static: use {.header.} and link the C objects at build time.
  • Dynamic: use {.dynlib.} and optionally specify a library name with dynlib: "libfoo".

Platform/architecture conditionals:

  • Use when defined(windows): etc. for library names and ABI differences.

Don’t accidentally change ABI rules:

  • Don’t reorder fields in structs.
  • Don’t use Nim’s default enums if C expects explicit integer sizes.
  • Don’t “helpfully” convert pointer types in the raw layer.

5. C→Nim Type Mapping (Table + Examples)

Mapping Table

C Type Nim Type Notes
int cint Exact C int width
unsigned int cuint
long clong Platform-dependent
unsigned long culong
long long clonglong
size_t csize_t Use for sizes
intptr_t cintPtr Pointer-sized int
uintptr_t cuintPtr
float cfloat
double cdouble
char* cstring NUL-terminated
const char* cstring Treat as read-only
void* pointer Generic pointer
T* ptr T Nullable by default
T** ptr ptr T

Structs and alignment

  • Use object with fields in C order.
  • Use packed only if C headers specify packing.
  • If alignment is unclear, add static: doAssert sizeof(T) == ... in tests.

Arrays

  • Fixed-size array in struct: array[N, T]
  • Pointer+length: ptr T + csize_t
  • Raw buffers: ptr UncheckedArray[T]

Opaque handles

  • Represent as pointer or ptr OpaqueObj where OpaqueObj is an empty object.

Example: fixed array in struct

C:

typedef struct LIB_Color {
  unsigned char rgba[4];
} LIB_Color;

Raw Nim:

type
  LibColor* {.importc: "LIB_Color".} = object
    rgba*: array[4, cuchar]

6. Wrapping Enums, Flags, and Constants

Enums:

  • Use explicit values to match C.
  • Use enum with explicit base type if needed.
typedef enum LIB_Mode {
  LIB_ModeA = 0,
  LIB_ModeB = 2
} LIB_Mode;
type
  LibMode* {.size: sizeof(cint), importc: "LIB_Mode".} = enum
    ModeA = 0,
    ModeB = 2

Bitflags:

  • Prefer set[Enum] if values are powers of two and max bit count is small.
  • Otherwise use a distinct integer type and helper procs.
type
  LibFlags* = distinct cuint

proc has*(flags: LibFlags; flag: LibFlags): bool {.inline.} =
  (flags.uint and flag.uint) != 0

Macros:

  • Simple numeric macros → const.
  • Function-like macros → wrap as inline procs or templates.

If a macro depends on sizeof or expressions with side effects, prefer a Nim template.


7. Wrapping Functions

Raw signatures must match C exactly.

Example: create/destroy

C:

typedef struct LIB_Handle LIB_Handle;
LIB_Handle* LIB_Create(int width, int height);
void LIB_Destroy(LIB_Handle* h);

Raw Nim:

type
  LibHandle* {.importc: "LIB_Handle".} = object

proc libCreate*(width, height: cint): ptr LibHandle
  {.importc: "LIB_Create", cdecl.}
proc libDestroy*(h: ptr LibHandle)
  {.importc: "LIB_Destroy", cdecl.}

Ergonomic Nim:

type
  Handle* = object
    raw*: ptr LibHandle

proc initHandle*(width, height: int): Handle =
  result.raw = libCreate(cint width, cint height)
  if result.raw.isNil:
    raise newException(ValueError, "Failed to create handle")

proc destroy*(h: var Handle) =
  if not h.raw.isNil:
    libDestroy(h.raw)
    h.raw = nil

Example: in/out parameters

C:

int LIB_GetSize(LIB_Handle* h, int* w, int* hgt);

Raw Nim:

proc libGetSize*(h: ptr LibHandle; w, hgt: ptr cint): cint
  {.importc: "LIB_GetSize", cdecl.}

Ergonomic Nim:

proc size*(h: Handle): tuple[w, hgt: int] =
  var wC, hC: cint
  if libGetSize(h.raw, addr wC, addr hC) != 0:
    raise newException(IOError, "LIB_GetSize failed")
  (int wC, int hC)

Example: string parameters

C:

int LIB_SetName(LIB_Handle* h, const char* name);

Raw Nim:

proc libSetName*(h: ptr LibHandle; name: cstring): cint
  {.importc: "LIB_SetName", cdecl.}

Ergonomic Nim:

proc setName*(h: Handle; name: string) =
  if libSetName(h.raw, name.cstring) != 0:
    raise newException(ValueError, "LIB_SetName failed")

8. Callbacks / Function Pointers

Declare callback types with cdecl:

typedef void (*LIB_LogFn)(void* userdata, const char* msg);
void LIB_SetLogFn(LIB_LogFn fn, void* userdata);

Raw Nim:

type
  LibLogFn* = proc(userdata: pointer; msg: cstring) {.cdecl.}

proc libSetLogFn*(fn: LibLogFn; userdata: pointer)
  {.importc: "LIB_SetLogFn", cdecl.}

Ergonomic Nim:

  • Avoid capturing closures. C expects a plain function pointer.
  • Store state in a global table keyed by userdata if needed.
proc logBridge(userdata: pointer; msg: cstring) {.cdecl.} =
  # Convert and dispatch safely
  let s = $msg
  discard s

proc setLogCallback*(fn: LibLogFn; userdata: pointer) =
  libSetLogFn(fn, userdata)

GC safety:

  • Don’t pass Nim closures to C as callbacks.
  • If you store Nim data for callbacks, ensure it is globally rooted or manually managed.
  • Consider gcsafe only if the callback never touches GC-managed data.

9. Memory Ownership, Lifetime, and Safety

Define ownership clearly:

  • Borrowed: C owns memory, caller must not free.
  • Owned: Nim wrapper takes responsibility to free.

Copy when needed:

  • Convert cstring output to Nim string if C may free/modify the buffer.

Resource management patterns:

  • Explicit destroy/free procs.
  • Optional RAII-like wrappers if C rules are stable and thread-safe.

Avoid leaks and double-free:

  • Set pointers to nil after freeing.
  • Don’t free memory you did not allocate.

10. Error Handling Patterns

C error reporting:

  • Return codes (0 / non-zero)
  • errno
  • Null pointers

Provide both layers:

  • Raw layer returns the exact codes.
  • Ergonomic layer raises exceptions or returns a Result-like tuple.
proc tryOpen*(path: string): tuple[ok: bool; handle: Handle] =
  var h = initHandle(1, 1)
  # Example only; map to real C functions
  (true, h)

Keep error behavior predictable and testable.


11. Testing & Verification Checklist

  • Compile check: ensure Nim compiles with the library headers.
  • Link check: confirm the library links (static or dynamic).
  • Smoke test: call one simple function.
  • ABI checks: sizeof, alignof, field offsets if possible.
  • Runtime checks: verify ownership rules and callback invocation.

Common failure symptoms:

  • Crashes at call sites → wrong calling convention or struct layout.
  • Garbage values → wrong integer width or alignment.
  • Random crashes → lifetime issues or freed memory.

12. Common Pitfalls (Concrete)

  • Wrong calling convention (cdecl vs stdcall).
  • Wrong integer widths (int vs long vs size_t).
  • Passing Nim string directly to C without .cstring.
  • Struct packing mismatch (missing packed, wrong field order).
  • Returning pointers to temporary memory or stack buffers.
  • Callbacks capturing GC-managed state or closures.
  • Using seq where C expects stable memory (reallocation risk).
  • Lifetime issues with cstring (temporary pointer invalid after call).

Example Set (Minimal, Generic)

A. Strings in/out

C:

const char* LIB_GetName(LIB_Handle* h);
int LIB_SetName(LIB_Handle* h, const char* name);

Raw Nim:

proc libGetName*(h: ptr LibHandle): cstring
  {.importc: "LIB_GetName", cdecl.}
proc libSetName*(h: ptr LibHandle; name: cstring): cint
  {.importc: "LIB_SetName", cdecl.}

Ergonomic Nim:

proc name*(h: Handle): string =
  let p = libGetName(h.raw)
  if p.isNil: return ""
  $p

proc setName*(h: Handle; name: string) =
  if libSetName(h.raw, name.cstring) != 0:
    raise newException(ValueError, "setName failed")

B. Pointer + length buffer

C:

int LIB_Read(LIB_Handle* h, unsigned char* out, size_t len);

Raw Nim:

proc libRead*(h: ptr LibHandle; outBuf: ptr cuchar; len: csize_t): cint
  {.importc: "LIB_Read", cdecl.}

Ergonomic Nim:

proc read*(h: Handle; buf: var openArray[byte]): int =
  if buf.len == 0: return 0
  let rc = libRead(h.raw, cast[ptr cuchar](addr buf[0]), csize_t buf.len)
  int rc

C. Create / destroy resource

C:

LIB_Handle* LIB_Open(const char* path);
void LIB_Close(LIB_Handle* h);

Raw Nim:

proc libOpen*(path: cstring): ptr LibHandle
  {.importc: "LIB_Open", cdecl.}
proc libClose*(h: ptr LibHandle)
  {.importc: "LIB_Close", cdecl.}

Ergonomic Nim:

proc open*(path: string): Handle =
  result.raw = libOpen(path.cstring)
  if result.raw.isNil:
    raise newException(IOError, "open failed")

proc close*(h: var Handle) =
  if not h.raw.isNil:
    libClose(h.raw)
    h.raw = nil

D. Callback registration

C:

typedef void (*LIB_OnEvent)(void* userdata, int code);
void LIB_SetOnEvent(LIB_OnEvent cb, void* userdata);

Raw Nim:

type
  LibOnEvent* = proc(userdata: pointer; code: cint) {.cdecl.}

proc libSetOnEvent*(cb: LibOnEvent; userdata: pointer)
  {.importc: "LIB_SetOnEvent", cdecl.}

Ergonomic Nim:

proc onEventBridge(userdata: pointer; code: cint) {.cdecl.} =
  discard userdata
  discard code

proc setOnEvent*(cb: LibOnEvent; userdata: pointer) =
  libSetOnEvent(cb, userdata)

Quick Do / Don’t

Do:

  • Keep raw bindings minimal and ABI-faithful.
  • Use cint, csize_t, cstring for C interop.
  • Provide safe wrappers that validate errors and manage resources.

Don’t:

  • Pass Nim string directly as char*.
  • Convert pointers in the raw layer.
  • Use closures for callbacks unless you fully manage lifetime and GC safety.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment