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
- DIRECT MEMORY CONTROL
- COMPILATION TO WEBASSEMBLY
- LOW-LEVEL OPERATIONS
- 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)-9 quintillion to ~9 quintillion)
int64_t // Exactly 64 bits (
// 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 5What 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
Use fixed-width integers (int64_t, uint8_t, etc.).
Never use plain int or long in Hooks.
All arrays must be stack-allocated with fixed sizes.
No malloc, no dynamic allocation.
Pointers are memory addresses.
The Hooks API uses them to read/write data; SBUF handles the casting.
Always check buffer bounds.
Buffer overflow is the most dangerous C bug.
Guards are mandatory for all loops.
GUARD(n) must appear before the loop condition. ---