Raylib C to Nim (naylib) Translation Guide
July 3, 2026 · View on GitHub
This document serves as a comprehensive guide for translating raylib C examples to Nim using the naylib wrapper. It details the patterns, idioms, and conventions used in the existing codebase to ensure consistency and maintainability.
1. File Structure and Organization
Basic File Template
Every example follows a consistent structure:
# ****************************************************************************************
#
# raylib [core] example - Basic window
#
# Example complexity rating: [★☆☆☆] 1/4
#
# Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
# BSD-like license that allows static linking with closed source software
#
# Copyright (c) 2013-2025 Ramon Santamaria (@raysan5)
#
# ****************************************************************************************
import raylib, std/[math, lenientops]
const
ScreenWidth = 800
ScreenHeight = 450
proc main =
# Initialization
initWindow(ScreenWidth, ScreenHeight, "raylib [core] example - basic window")
defer: closeWindow() # Important pattern in naylib
setTargetFPS(60)
# Main game loop
while not windowShouldClose():
# Update
# TODO: Update variables here
# Draw
drawing(): # Using template for safer begin-end pairs
clearBackground(RayWhite)
drawText("Congrats! You created your first window!", 190, 200, 20, LightGray)
main()
Key Points
- Standard Header Comment: Always include the full original raylib header, adapted to Nim's comment syntax
- Import Section: Include only the necessary naylib (
raylib,raymath, etc.) and Nim standard library modules used in the code - Nim Alternatives: Use Nim standard library or packages for text, file, compression, and encoding utilities.
- Function Overloading: Remove any raylib suffixes (
V,Ex,Rec,Pro, etc.) and rely on Nim's overload resolution. - Constants Section: Screen dimensions and other constants at the top
- Main Procedure: All logic encapsulated in a
main()procedure - Initialization Block: Window initialization with
deferfor cleanup - Game Loop: Standard while loop with update/draw sections
- Resource Management: Rely on automatic destructors for naylib objects like
TextureorFont
2. Naming Conventions
Variables and Procedures
- Use
camelCasefor variables and procedure names:var framesCounter: int32 = 0 proc updateCamera() = ...
Constants
- Use
PascalCasefor constants:const MaxFrameSpeed = 15 MinFrameSpeed = 1 ScreenWidth = 800
Types
- Use
PascalCasefor type names:type LightKind = enum Directional, Point, Spot
3. Type Translations
Number Types
C numeric types are mapped to Nim types following these patterns:
# C: float -> Nim: float32
let posX: float32 = 22.5 # Explicit type required
# C: int -> Nim: int32
let counter: int32 = 0 # Explicit type required
# C: unsigned int -> Nim: uint32
let flags: uint32 = 0 # Explicit type required
Nim Defaults:
- Float literals like
2.0are float64. Integer literals are polymorphic. float64andfloat32are implicitly convertible both ways; int literals convert to many numeric types.
Rules:
-
No suffixes needed for simple literals If you specify the type (e.g.,
: float32), don't add'f32or similar suffixes — they are redundant. -
Use whole numbers when possible Write whole numbers without a decimal point (e.g.,
45instead of45.0), even if the target type is a float. -
Prevent unintended float widening or mismatches Use literal suffixes (
'f32) or explicit conversion (float32(value)), (e.g.,lineThick*0.5'f32,float32(screenWidth)/2) to ensure float32 precision. -
Avoid C-Style Suffixes Do not use C-style suffixes like
0.0f.
Good:
let a: float32 = 15 # No suffix needed
let b: float32 = 0.2 # No suffix needed
let angle: float32 = 180 # Whole number for float type
let angle = degToRad(170'f32) # Add type hints when the type is ambiguous
let result1 = lineThick*0.5'f32 # This prevents unintended float widening
let result2 = screenWidth/2'f32 # This prevents unintended float widening
let result3 = float32(screenWidth)/2 # This prevents unintended float widening
let ratio1: float32 = float32(intValue1) / intValue2 # Convert at least one operand to float32
let ratio2: float32 = float32(intValue1) / float32(intValue2)
Bad:
let a: float32 = 15'f32 # Redundant suffix
let b: float32 = 0.2'f32 # Redundant suffix
let angle: float32 = degToRad(170) # May fail due to missing type hint
let result1 = lineThick*0.5 # Unintended float widening to 64-bit
let result3 = screenWidth/2 # The type is inferred as float (64-bit)
let result3 = screenWidth/2.0 # Explicit decimal creates 64-bit float
let ratio: float32 = intValue1 / intValue2 # Expression evaluates to float64, gets down-converted
Mapping C Types to Nim Types
| C Type | Nim Type | Notes |
|---|---|---|
int | int32 | Explicitly use 32-bit integers |
short | int16 | The standard for 16-bit integers |
long long | int64 | The standard for 64-bit integers |
unsigned int | uint32 | For non-negative 32-bit integers |
float | float32 | Explicitly use 32-bit floats |
double | float or float64 | Nim's default float is a 64-bit double |
bool | bool | Direct translation |
char*, const char* | string | Replace C strings with Nim's safe, managed strings. |
void* (generic data) | generics | Use type-safe generics instead of raw pointers. |
struct T | type T = object | Direct translation to Nim's object type. |
enum E | type E = enum | Direct translation to Nim's safer enum type. |
T* (heap object) | ref T | For pointers to shared, managed objects. |
T* (out-parameter) | var T | For modifying values in-place (pass-by-reference). |
T* (array) | seq[T] | Replace C-style arrays with Nim's dynamic sequences. |
T arr[N] (fixed array) | array[N, T] | Use for fixed-size, stack-allocated arrays. |
Creating Struct Instances
# Good: This is the standard, clear way to create objects in Nim.
var camera = Camera(
position: Vector3(x: 5, y: 5, z: 5),
target: Vector3(x: 0, y: 0, z: 0),
up: Vector3(x: 0, y: 1, z: 0),
fovy: 45,
projection: Perspective
)
# Also works, but is less ideal:
var camera: Camera
camera.position = Vector3(x: 5, y: 5, z: 5)
camera.target = Vector3(x: 0, y: 0, z: 0)
camera.up = Vector3(x: 0, y: 1, z: 0)
camera.fovy = 45
camera.projection = Perspective
4. Function Call Patterns
Function Calls
Translate raylib C functions to Nim by removing any raylib suffix (V, Ex, Rec, Pro, etc.) if present; leave functions without suffixes unchanged. Leaving a suffix on an overloaded function will produce incorrect Nim code.
InitWindow(screenWidth, screenHeight, "Title");
DrawSphereEx(centerPos, radius, rings, slices, RED);
DrawRectanglePro(sourceRec, destRec, origin, 45.0f, GREEN);
DrawCircleLinesV(center, 30, BLUE);
DrawTextureRec(texture, sourceRec, position, WHITE);
initWindow(screenWidth, screenHeight, "Title") # Unchanged, no suffix to remove
drawSphere(centerPos, radius, rings, slices, Red) # Removed the "Ex" suffix
drawRectangle(sourceRec, destRec, origin, 45, Green) # Removed the "Pro" suffix
drawCircleLines(center, 30, Blue) # Removed the "V" suffix
drawTexture(texture, sourceRec, position, White) # Removed the "Rec" suffix
Nim Alternatives
For raylib functions not wrapped in naylib (text handling, file I/O, compression, encoding, etc.), prefer Nim standard library functions or recommended external packages. A complete reference is available in https://github.com/planetis-m/naylib/blob/main/manual/alternatives_table.rst.
GetRandomValue(0, 10);
float angle = DEG2RAD*90.0f;
if (FileExists("data.txt")) { ... }
int n = TextLength("hello");
let value = rand(0..10)
let angle = degToRad(90'f32)
if fileExists("data.txt"): ...
let n = "hello".len
5. Control Flow Patterns
Input Handling
Use direct translations of input functions with camelCase naming and Nim's boolean expressions:
if (IsKeyPressed(KEY_SPACE))
// Handle space key press
if (IsKeyDown(KEY_LEFT))
// Handle left key being held down
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT))
// Handle left mouse button press
if isKeyPressed(Space):
# Handle space key press
if isKeyDown(Left):
# Handle left key being held down
if isMouseButtonPressed(Left):
# Handle left mouse button press
Conditional Drawing
Use templates for scoped operations:
drawing(): # Equivalent to beginDrawing()/endDrawing()
clearBackground(RayWhite)
# Drawing code here
mode3D(camera): # Equivalent to beginMode3D()/endMode3D()
drawModel(model, position, scale, White)
6. Memory Management
Automatic Cleanup with Destructors
Naylib uses destructors for automatic memory management of types like Image, Wave, Texture, etc. This eliminates the need for manual Unload calls:
let texture = loadTexture("image.png")
# No need to manually unload - automatically cleaned up by destructor
Using Explicit Cleanup
For cases where you want explicit control or need to ensure cleanup at a specific point:
var image = loadImage("resources/heightmap.png") # Load image (RAM)
let texture = loadTextureFromImage(image) # Convert image to texture (VRAM)
reset(image) # Unload image from RAM, already uploaded to VRAM
7. Mathematical Operations
When translating raymath functions from C to Nim, use operators where available and drop the type prefixes from function names.
Vector3 sum = Vector3Add(a, b);
Vector3 diff = Vector3Subtract(a, b);
Vector3 scale = Vector3Scale(a, factor);
Vector3 mul = Vector3Multiply(a, b);
Vector3 div = Vector3Divide(a, b);
Vector3 trans = Vector3Transform(a, matrix);
float dist = Vector3Distance(a, b);
Vector3 neg = Vector3Negate(a);
Quaternion q = QuaternionMultiply(q1, q2);
if (Vector3Equals(a, b));
let sum = a + b
let diff = a - b
let scale = a*factor
let mul = a*b
let `div` = a/b
let trans = a*matrix
let dist = distance(a, b)
let neg = -a
let q = q1*q2
if a =~ b: discard # approximate equality
Using std/lenientops for Mixed-Type Arithmetic
Import std/lenientops to allow direct arithmetic between ints and floats avoiding repetitive type conversions.
import std/lenientops
var
count: int32 = 10
scaleFactor: float32 = 3.5
offset: int32 = 100
adjustment: float32 = 50.5
# Without lenientops, you'd need explicit casts:
let result1 = float32(count)*scaleFactor
let result2 = offset + int32(adjustment)
# With lenientops, direct operations work:
let result1 = count*scaleFactor # Works directly
let result2 = offset + adjustment # Works directly
Raylib Style Arithmetic Spacing
Following raylib's coding style, omit spaces around * and /, but include spaces around + and -.
# Good raylib style
let centerX = screenWidth/2'f32 - buttonWidth/2'f32
let centerY = screenHeight/2'f32 - buttonHeight/2'f32
let scaledValue = baseValue*1.5'f32
# Less preferred
let centerX = screenWidth / 2'f32 - buttonWidth / 2'f32
let centerY = screenHeight / 2'f32 - buttonHeight / 2'f32
let scaledValue = baseValue * 1.5'f32
Splitting Long Expressions in Nim
- Break lines only after binary operators, commas, or open parentheses.
- Place binary operators at the end of the line, not at the start of the next line.
- Indent continuation lines consistently.
- Unary operators (e.g., a leading
-for a negative term) may appear at the start of a line.
# Incorrect (causes errors):
let a1 = (-G*(2*m1 + m2)*sin(theta1)
- m2*G*sin(theta1 - 2*theta2)
- 2*sinD*m2*(ww2*L2 + ww1*L1*cosD))
/ (L1*(2*m1 + m2 - m2*cos2D))
# Correct:
let a1 = (-G*(2*m1 + m2)*sin(theta1) -
m2*G*sin(theta1 - 2*theta2) -
2*sinD*m2*(ww2*L2 + ww1*L1*cosD)) /
(L1*(2*m1 + m2 - m2*cos2D))
8. Error Handling
Resource Loading Validation
Naylib's load* functions automatically validate asset loading and raise RaylibError if they fail:
# This will automatically raise RaylibError if loading fails
let texture = loadTexture("image.png")
# We skip explicit error handling in the examples for brevity.
Assertions
Use assertions for preconditions:
import std/assertions
assert(windowIsReady(), "Window should be initialized")
9. String Formatting and Text
Formatted text drawing
Use Nim string interpolation:
DrawText(TextFormat("TARGET FPS: %i", targetFPS), x, y, fontSize, color);
import std/strformat
drawText(&"TARGET FPS: {targetFPS}", x, y, fontSize, color)
8. Audio Patterns
Audio Device Management
initAudioDevice()
defer: closeAudioDevice() # Still needed as it's a global resource
9. Shader Patterns
Shader Loading
when defined(GraphicsApiOpenGl33):
const GlslVersion = 330
else:
const GlslVersion = 100
let fragShaderFileName = &"resources/shaders/glsl{GlslVersion}/reload.fs"
Shader Value Setting
int colorLoc = GetShaderLocation(shader, "color");
float color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
SetShaderValue(shader, colorLoc, color, SHADER_UNIFORM_VEC4); // must pass the uniform type explicitly
let colorLoc = getShaderLocation(shader, "color")
let color: array[4, float32] = [1, 0, 0, 1]
setShaderValue(shader, colorLoc, color) # uniform type inferred as Vec4
setShaderValue infers the uniform type from the Nim value at compile time (e.g., float32, array[3, float32], array[4, int32]) and forwards it to the low-level implementation, so you don’t need to pass the uniform type explicitly.
10. Flag Patterns
Setting Configuration Flags
Use the flags procedure to work with bitflags like ConfigFlags:
SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_HIGHDPI)
setConfigFlags(flags(Msaa4xHint, WindowHighdpi))