Lesson 6: C Programming for Hooks - Essential Concepts | Hooks & Smart Contracts | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
beginner65 min

Lesson 6: C Programming for Hooks - Essential Concepts

Learning Objectives

Use fixed-width integer types appropriately (int64_t, uint8_t, etc.)

Work with arrays and buffers using stack memory only

Understand pointers enough to use the Hooks API correctly

Avoid common C pitfalls that cause bugs or security vulnerabilities

Read and write C code patterns used throughout Hook development

You might wonder why Hooks use C instead of something more modern. The reasons are practical:

WHY C FOR HOOKS
  1. DIRECT MEMORY CONTROL
  1. COMPILATION TO WEBASSEMBLY
  1. LOW-LEVEL OPERATIONS
  1. HISTORICAL REASONS

You don't need to love C. You need to be effective in it for this specific domain.


C's basic types (int, long) have platform-dependent sizes. Hooks use fixed-width integers:

// Fixed-width integer types - ALWAYS use these in Hooks

// Signed integers
int8_t // Exactly 8 bits (-128 to 127)
int16_t // Exactly 16 bits (-32768 to 32767)
int32_t // Exactly 32 bits (-2B to ~2B)
int64_t // Exactly 64 bits (
-9 quintillion to ~9 quintillion)

// Unsigned integers
uint8_t // Exactly 8 bits (0 to 255)
uint16_t // Exactly 16 bits (0 to 65535)
uint32_t // Exactly 32 bits (0 to ~4B)
uint64_t // Exactly 64 bits (0 to ~18 quintillion)

// Common uses in Hooks
uint8_t buffer[32]; // Byte buffer (e.g., account ID, hash)
int64_t drops; // XRP amounts (always in drops)
uint32_t length; // Buffer/string lengths
int64_t result; // API return values
```

XRP is always handled in drops (1 XRP = 1,000,000 drops):

// XRP amount handling

// Constants
#define DROPS_PER_XRP 1000000

// Converting XRP to drops
int64_t xrp = 100;
int64_t drops = xrp * DROPS_PER_XRP;  // 100,000,000 drops

// Common amounts
int64_t one_xrp    = 1000000;       // 1 XRP
int64_t ten_xrp    = 10000000;      // 10 XRP
int64_t hundred    = 100000000;     // 100 XRP

// Reading amount from transaction
int64_t amount = otxn_amount();     // Returns drops (int64_t)

// Comparison
if (amount < 10000000) {  // Less than 10 XRP
    rollback(SBUF("Minimum 10 XRP required"), 1);
}

Hooks API functions return int64_t with specific meanings:

// Hooks API return value conventions

// Positive values: Success (often length of data)
int64_t len = otxn_field(buffer, sizeof(buffer), sfAmount);
if (len > 0) {
    // Success! len is the number of bytes written
}

// Zero: Varies by function (usually success or "not found")

// Negative values: Error codes
if (len < 0) {
    // Error! len contains error code
    // Common errors:
    // -1: General error
    // -2: Out of bounds
    // -3: Invalid argument
    // -4: Not found
}

// Pattern: Always check return values
int64_t result = some_api_call(...);
if (result < 0) {
    rollback(SBUF("API call failed"), (uint32_t)result);
}

Hooks cannot use heap memory. All arrays must be on the stack with fixed sizes:

// CORRECT: Fixed-size stack arrays
uint8_t small_buffer[32];    // 32 bytes
uint8_t account_id[20];      // Account IDs are 20 bytes
uint8_t hash[32];            // Hashes are 32 bytes
uint8_t tx_buffer[256];      // Transaction buffer

// WRONG: Dynamic allocation (will not compile)
uint8_t* dynamic = malloc(32);  // NO! No heap allocation
uint8_t* variable;              // NO! Must initialize with size

// WRONG: Variable-length arrays (may not work)
int size = get_size();
uint8_t vla[size];             // NO! Size must be compile-time constant

Choose buffer sizes carefully:

// Common buffer sizes in Hooks

// Identity
uint8_t account_id[20];    // All account IDs
uint8_t public_key[33];    // Compressed public key

// Hashes
uint8_t hash[32];          // SHA-512Half results

// Transactions
uint8_t small_tx[128];     // Simple transactions
uint8_t medium_tx[256];    // Most transactions
uint8_t large_tx[512];     // Complex transactions

// Serialized objects
uint8_t keylet[34];        // Ledger object keylets
uint8_t amount[48];        // Amount objects (XRP or IOU)

// State
uint8_t state_key[32];     // State keys (max 32 bytes)
uint8_t state_value[256];  // State values (max 256 bytes)

// Memos
uint8_t memo[256];         // Memo content varies

SBUF simplifies buffer passing to APIs:

// SBUF expands to: pointer, sizeof(pointer)

// Without SBUF:
uint8_t message[] = "Hello";
accept(message, sizeof(message) - 1, 0);  // -1 for null terminator

// With SBUF:
accept(SBUF("Hello"), 0);  // Cleaner!

// How SBUF works (simplified):
#define SBUF(x) (uint32_t)(x), sizeof(x)

// More examples:
uint8_t buffer[32];
trace(SBUF("Debug message"), 0);     // String literal
state_set(SBUF(buffer), SBUF(key));  // Buffer variables

Working with buffers without standard library:

// Zeroing a buffer (important before use)
uint8_t buffer[32];
for (int i = 0; GUARD(32), i < 32; ++i)
    buffer[i] = 0;

// Or using Hooks API:
CLEARBUF(buffer);  // If available in your version

// Comparing buffers
uint8_t a[20], b[20];
int equal = 1;
for (int i = 0; GUARD(20), i < 20; ++i) {
    if (a[i] != b[i]) {
        equal = 0;
        break;
    }
}

// Using BUFFER_EQUAL macro:
int result = 0;
BUFFER_EQUAL(result, a, b, 20);
// result is now 1 if equal, 0 if not

// Copying buffers
uint8_t src[20], dst[20];
for (int i = 0; GUARD(20), i < 20; ++i)
    dst[i] = src[i];

A pointer is a variable that holds a memory address:

// Basic pointer concept

int64_t value = 42;        // A regular variable
int64_t* ptr = &value;     // A pointer to that variable

// ptr contains the ADDRESS where 42 is stored
// *ptr gives us the VALUE at that address (42)

// Visualization:
// 
// Memory:
// Address    Value
// 0x1000     42         <- value lives here
// 0x1008     0x1000     <- ptr contains address of value

The Hooks API uses pointers to pass data:

// Many Hooks APIs use this pattern:
int64_t api_function(
    uint32_t write_ptr, uint32_t write_len,  // Output: where to write
    uint32_t read_ptr, uint32_t read_len     // Input: where to read
);

// Example: state_set
uint8_t value[] = "Hello";
uint8_t key[] = "mykey";

int64_t result = state_set(
    (uint32_t)value, sizeof(value),  // Value to store
    (uint32_t)key, sizeof(key)       // Key to store under
);

// The casts to uint32_t convert pointer to integer
// This is how WASM handles memory addresses
// In C, a pointer is a memory address
// In WASM, memory is a linear byte array accessed by index
// The Hooks API expects uint32_t indices, not C pointers

uint8_t buffer[32];

// buffer is a pointer (memory address in C)
// (uint32_t)buffer casts it to an integer (memory index for WASM)

// This is why you see:
int64_t len = otxn_field(
(uint32_t)buffer, // Destination address as integer
sizeof(buffer), // How many bytes to write
sfAmount // Which field to read
);

// SBUF does this cast for you:
// SBUF(buffer) expands to (uint32_t)buffer, sizeof(buffer)
```

// Pattern 1: Output buffer
uint8_t output[32];
int64_t result = some_api(SBUF(output), ...);
// API writes into output, returns number of bytes written

// Pattern 2: Input data
uint8_t input[] = "data";
int64_t result = some_api(..., SBUF(input));
// API reads from input

// Pattern 3: Both input and output
uint8_t out[32];
uint8_t in[20];
int64_t result = transform(SBUF(out), SBUF(in));
// API reads from in, writes to out

// Pattern 4: Optional output (just get length)
int64_t len = otxn_field(0, 0, sfMemos);
// Passing 0, 0 means "just tell me the length"
// Useful to check if field exists before allocating buffer
```


The most dangerous C bug:

// DANGEROUS: Buffer overflow
uint8_t small[10];
uint8_t* source = get_data();  // Returns 100 bytes

for (int i = 0; i < 100; ++i) {
    small[i] = source[i];  // OVERFLOW! Writing past buffer end
}

// SAFE: Check bounds
uint8_t small[10];
int64_t source_len = get_data_length();

int64_t copy_len = source_len < 10 ? source_len : 10;
for (int i = 0; GUARD(10), i < copy_len; ++i) {
    small[i] = source[i];
}

// SAFEST: Size buffer appropriately
uint8_t adequate[256];  // Large enough for expected data
// DANGEROUS: Uninitialized data
uint8_t buffer[32];
// buffer contains GARBAGE - whatever was in memory

if (buffer[0] == 0) { // Unpredictable!
// ...
}

// SAFE: Always initialize
uint8_t buffer[32] = {0}; // Zero-initialized

// Or clear explicitly
uint8_t buffer[32];
for (int i = 0; GUARD(32), i < 32; ++i)
buffer[i] = 0;
```

// DANGEROUS: Integer overflow
uint32_t a = 4000000000;
uint32_t b = 4000000000;
uint32_t sum = a + b;  // OVERFLOW! Wraps to ~3.7B

// SAFE: Check before operation
if (a > UINT32_MAX - b) {
// Would overflow
rollback(SBUF("Amount too large"), 1);
}
uint32_t sum = a + b;

// Or use larger type
uint64_t safe_sum = (uint64_t)a + (uint64_t)b;

// COMMON IN HOOKS: XRP amounts
int64_t amount = otxn_amount();
int64_t fee = 1000000; // 1 XRP
int64_t total = amount + fee;

// Check for overflow if amounts could be very large
if (amount > INT64_MAX - fee) {
rollback(SBUF("Amount overflow"), 1);
}
```

// DANGEROUS: Off-by-one
uint8_t buffer[10];
for (int i = 0; i <= 10; ++i) {  // <= should be <
    buffer[i] = 0;  // Writes 11 bytes, buffer is only 10!
}

// SAFE: Use < for array bounds
for (int i = 0; GUARD(10), i < 10; ++i) {
buffer[i] = 0;
}

// COMMON MISTAKE: Forgetting null terminator
char message[5] = "hello"; // Needs 6 bytes! (5 chars + null)
// Should be: char message[6] = "hello";
// Or: char message[] = "hello"; // Compiler adds null
```


#include "hookapi.h"

// Hook entry point
int64_t hook(uint32_t reserved) {
_g(1, 1); // Guard for main function

// Your logic here

accept(SBUF("Success"), 0);
return 0;
}

// Callback entry point
int64_t cbak(uint32_t reserved) {
return 0;
}
```

// Guards are REQUIRED for all loops

// Basic for loop
for (int i = 0; GUARD(10), i < 10; ++i) {
// Loop body (max 10 iterations)
}

// While loop
int count = 0;
while (GUARD(100), condition && count < 100) {
// Loop body
count++;
}

// Nested loops - multiply guards!
for (int i = 0; GUARD(10), i < 10; ++i) {
for (int j = 0; GUARD(10), j < 10; ++j) {
// Inner loop: 10 * 10 = 100 max total iterations
}
}

// Guard value must be literal, not variable
int n = 10;
for (int i = 0; GUARD(n), i < n; ++i) // WRONG! n is variable
for (int i = 0; GUARD(10), i < n; ++i) // OK if n <= 10
```

// Getting transaction fields

// Step 1: Declare buffer
uint8_t dest[20]; // Account IDs are 20 bytes

// Step 2: Call otxn_field with sfCode
int64_t dest_len = otxn_field(SBUF(dest), sfDestination);

// Step 3: Check result
if (dest_len != 20) {
rollback(SBUF("Failed to get destination"), 1);
}

// Now dest contains the destination account ID

// Common sfCodes:
// sfAccount - Source account (20 bytes)
// sfDestination - Destination account (20 bytes)
// sfAmount - Amount (8 bytes XRP, 48 bytes IOU)
// sfMemos - Memo array (variable)
// sfTransactionType - Transaction type (2 bytes)
```

// Reading state
uint8_t value[256];
uint8_t key[] = "counter";

int64_t len = state(SBUF(value), SBUF(key));

if (len < 0) {
// State doesn't exist yet, initialize
value[0] = 0;
}

// Writing state
uint8_t new_value[] = "updated";
uint8_t key[] = "mykey";

int64_t result = state_set(SBUF(new_value), SBUF(key));
if (result < 0) {
rollback(SBUF("Failed to write state"), 1);
}
```

// Emitting a transaction

// Step 1: Reserve emit slots
etxn_reserve(1); // We'll emit 1 transaction

// Step 2: Build the transaction
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
PREPARE_PAYMENT_SIMPLE(
tx, // Output buffer
1000000, // Amount: 1 XRP in drops
dest, // Destination account (uint8_t[20])
0, 0 // Source and destination tags (0 = none)
);

// Step 3: Emit
uint8_t emithash[32];
int64_t result = emit(SBUF(emithash), SBUF(tx));

if (result < 0) {
rollback(SBUF("Emission failed"), 1);
}

// emithash now contains the hash of the emitted transaction
```


// Use trace() liberally during development

int64_t hook(uint32_t reserved) {
_g(1, 1);

trace(SBUF("Hook started"), 0);

int64_t amount = otxn_amount();
trace(SBUF("Amount:"), amount);

uint8_t dest[20];
int64_t dest_len = otxn_field(SBUF(dest), sfDestination);
trace(SBUF("Dest len:"), dest_len);

if (amount < 1000000) {
trace(SBUF("Rejecting: amount too small"), amount);
rollback(SBUF("Minimum 1 XRP"), 1);
}

trace(SBUF("Accepting transaction"), 0);
accept(SBUF("OK"), 0);
return 0;
}
```

// ERROR: "undefined reference to 'printf'"
printf("Debug\n");  // Standard library not available!
// FIX: Use trace() instead
trace(SBUF("Debug"), 0);

// ERROR: "guard required"
for (int i = 0; i < 10; ++i) // Missing GUARD
// FIX:
for (int i = 0; GUARD(10), i < 10; ++i)

// ERROR: "undeclared identifier 'sfAmount'"
otxn_field(SBUF(buf), sfAmount); // sfcodes.h not included
// FIX: Ensure sfcodes.h is included (usually via hookapi.h)

// ERROR: "memory allocation functions are not allowed"
char* buf = malloc(100); // No heap!
// FIX: Use stack arrays
char buf[100];
```


C's direct memory control fits Hooks' constraints. Stack-only memory model maps naturally to C's capabilities.

Fixed-width integers prevent platform surprises. int64_t is always 64 bits, regardless of platform.

The patterns are learnable. You don't need all of C—just these specific patterns.

⚠️ Whether easier languages will become available. JSHooks in development; other options may emerge.

⚠️ Long-term best practices. The community is still developing idioms and conventions.

🔴 Buffer overflows. The most common security vulnerability in C. Always check bounds.

🔴 Assuming C knowledge from other languages. JavaScript's "arrays" are nothing like C arrays.

🔴 Skipping initialization. Uninitialized data causes unpredictable behavior.

🔴 Integer overflow in amount calculations. XRP amounts can be large; check for overflow.

C is harder than modern languages, but for Hooks you need only a subset. Master the patterns in this lesson—fixed integers, buffer handling, pointer basics, guards, and memory safety—and you'll be effective. Most Hook bugs come from the same few mistakes: buffer overflow, missing guards, uninitialized data. Know these pitfalls and you'll avoid most problems.


Assignment: Write a Hook that demonstrates mastery of C patterns covered in this lesson.

Requirements:

Create a Hook that:

  • Reads the transaction amount

  • Converts to XRP (from drops)

  • Rejects if less than 5 XRP

  • Logs the amount using trace()

  • Extracts the sender's account ID

  • Extracts the destination account ID

  • Compares them (reject if same—self-payment)

  • Logs both account IDs (using trace with appropriate info)

  • Maintains a counter of accepted transactions

  • Reads current counter from state

  • Increments counter

  • Writes new counter to state

  • Logs the counter value

  • If counter reaches 10, reject all further transactions

  • Log reason for rejection

  • Properly handle all edge cases

  • Complete Hook code (.c file)

  • Compilation successful (screenshot)

  • Deployment successful (transaction hash)

  • Test showing counter incrementing (transaction hashes)

  • Test showing rejection at counter = 10

  • Correct integer handling (20%)

  • Correct buffer operations (20%)

  • Correct state operations (20%)

  • All guards present (15%)

  • No memory safety issues (15%)

  • Code compiles and runs (10%)

Time investment: 3-4 hours
Value: Practical C skills for all future Hook development


Knowledge Check

Question 1 of 5

What type should you use to store XRP amounts in drops?

  • "The C Programming Language" by Kernighan and Ritchie (classic reference)
  • Learn-C.org (interactive tutorials)
  • "Expert C Programming" by Peter van der Linden
  • OWASP Buffer Overflow Guide
  • "Secure Coding in C and C++" by Robert Seacord
  • xrpl-hooks.readme.io/docs (API reference)
  • Hooks examples in Hooks Builder

For Next Lesson:
We'll explore the complete Hooks API—all the functions available for reading transactions, accessing ledger state, managing Hook state, and emitting transactions.


End of Lesson 6

Total words: ~4,500
Estimated completion time: 65 minutes reading + 3-4 hours for deliverable

Key Takeaways

1

Use fixed-width integers (int64_t, uint8_t, etc.).

Never use plain int or long in Hooks.

2

All arrays must be stack-allocated with fixed sizes.

No malloc, no dynamic allocation.

3

Pointers are memory addresses.

The Hooks API uses them to read/write data; SBUF handles the casting.

4

Always check buffer bounds.

Buffer overflow is the most dangerous C bug.

5

Guards are mandatory for all loops.

GUARD(n) must appear before the loop condition. ---