Hopefullly this may help some of you..
Mojo FFI Solutions & Binding Techniques Guide
Complete documentation of FFI issues solved and binding patterns developed
Table of Contents
- Critical FFI Issues Solved
- String Handling Solutions
- Function Signature Patterns
- Memory Management
- Event System Implementation
- Advanced Binding Techniques
- Performance Optimizations
- Common Pitfalls & Solutions
Critical FFI Issues Solved
1. String Array Crashes - SOLVED
Problem: Mojo crashed with pop.string.concat
illegal operation when passing string arrays.
Root Cause: Mojoβs string handling and array concatenation in FFI context caused segfaults.
Solution: Character-by-character string building with individual function calls.
# β BROKEN - Causes segfaults
fn register_string_broken(gui: MojoGUI, text: String, string_id: Int32) raises:
# This crashes Mojo with string array operations
var chars = List[Int32]()
for i in range(len(text)):
chars.append(ord(text[i]))
# Passing arrays causes crashes
# β
WORKING - Character-by-character approach
fn register_string(gui: MojoGUI, text: String, string_id: Int32) raises:
"""Safe string registration - character by character"""
_ = gui.StrClear(string_id)
for i in range(len(text)):
_ = gui.StrRegister(ord(text[i]), i, string_id)
Key Insight: Avoid complex data structures in FFI. Use simple, individual function calls.
2. Exception Handling Incompatibility - SOLVED
Problem: Python-style try/except not supported in Mojo FFI context.
# β BROKEN - Python-style exceptions
try:
var result = gui.SomeFunction()
except:
print("Error occurred") # This doesn't work in Mojo
# β
WORKING - Return code checking
var result = gui.SomeFunction()
if result < 0:
print("Error occurred")
Solution: Use return codes and conditional checking instead of exception handling.
3. Function Signature Complexity - SOLVED
Problem: Complex C function signatures caused FFI binding issues.
Solution: Simplify all C functions to use basic types only.
// β COMPLEX - Hard to bind
int ComplexFunction(struct ComplexData* data, char** string_array, int* results);
// β
SIMPLE - Easy to bind
int SimpleFunction(int param1, float param2, int string_id);
String Handling Solutions
Core String Management Pattern
C Implementation:
// Global string storage - simple and reliable
static char strings[MAX_STRINGS][MAX_STRING_LEN];
int StrClear(int string_id) {
if (string_id < 0 || string_id >= MAX_STRINGS) return -1;
memset(strings[string_id], 0, MAX_STRING_LEN);
return 0;
}
int StrRegister(int ch, int position, int string_id) {
if (string_id < 0 || string_id >= MAX_STRINGS) return -1;
if (position < 0 || position >= MAX_STRING_LEN - 1) return -1;
strings[string_id][position] = (char)ch;
strings[string_id][position + 1] = '\0'; // Always null-terminate
return 0;
}
Mojo Binding:
fn StrClear(self, string_id: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32) -> Int32]("StrClear")
return f(string_id)
fn StrRegister(self, ch: Int32, position: Int32, string_id: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32, Int32, Int32) -> Int32]("StrRegister")
return f(ch, position, string_id)
Helper Function:
fn register_string(gui: MojoGUI, text: String, string_id: Int32) raises:
"""Safe string registration helper"""
_ = gui.StrClear(string_id)
for i in range(len(text)):
_ = gui.StrRegister(ord(text[i]), i, string_id)
String Storage Architecture
String Management System:
βββββββββββββββββββββββββββββββββββββββ
β Mojo Layer β
β βββββββββββββββ βββββββββββββββββββ β
β βString Input β β register_string β β
β β"Hello" β β Helper β β
β βββββββββββββββ βββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
β char-by-char
βΌ
βββββββββββββββββββββββββββββββββββββββ
β FFI Layer β
β StrRegister('H', 0, id) β
β StrRegister('e', 1, id) β
β StrRegister('l', 2, id) β
β StrRegister('l', 3, id) β
β StrRegister('o', 4, id) β
βββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β C Layer β
β strings[id] = "Hello\0" β
β Stored safely in char array β
βββββββββββββββββββββββββββββββββββββββ
Function Signature Patterns
1. Basic Value Parameters
# Pattern: Simple types only
fn DrawSetColor(self, r: Float32, g: Float32, b: Float32, a: Float32) raises -> Int32:
var f = self.handle.get_function[fn(Float32, Float32, Float32, Float32) -> Int32]("DrawSetColor")
return f(r, g, b, a)
2. String Parameters (Direct)
# Pattern: Direct string pointer passing
fn DrawTextDirect(self, text: String, font_size: Float32) raises -> Int32:
var f = self.handle.get_function[fn(UnsafePointer[Int8], Float32) -> Int32]("DrawTextDirect")
return f(text.unsafe_ptr(), font_size)
3. Complex Parameters (Flattened)
# Pattern: Flatten complex structures into individual parameters
fn ScrollbarHandleMouseDown(self, scrollbar_id: Int32, mouse_x: Float32, mouse_y: Float32,
track_x: Float32, track_y: Float32, track_width: Float32,
track_height: Float32, vertical: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32, Float32, Float32, Float32, Float32, Float32, Float32, Int32) -> Int32]("ScrollbarHandleMouseDown")
return f(scrollbar_id, mouse_x, mouse_y, track_x, track_y, track_width, track_height, vertical)
4. Return Value Patterns
# Pattern: Different return types
fn EventGetKey(self) raises -> Int32: # Integer return
var f = self.handle.get_function[fn() -> Int32]("EventGetKey")
return f()
fn FontGetLineHeight(self, font_id: Int32) raises -> Float32: # Float return
var f = self.handle.get_function[fn(Int32) -> Float32]("FontGetLineHeight")
return f(font_id)
Memory Management
Safe Memory Patterns
1. Stack-Based Storage (Preferred):
// β
SAFE - Stack allocated, no malloc/free issues
static char strings[MAX_STRINGS][MAX_STRING_LEN];
static FontFace fonts[MAX_FONT_STYLES];
static TextEditState text_edits[MAX_TEXT_EDITORS];
2. Controlled Dynamic Allocation:
// β
SAFE - Single allocation with cleanup
unsigned char *font_bitmap = calloc(FONT_TEXTURE_SIZE * FONT_TEXTURE_SIZE * 4, 1);
// Always paired with cleanup
void cleanup() {
if (font_bitmap) {
free(font_bitmap);
font_bitmap = NULL;
}
}
3. OpenGL Resource Management:
// β
SAFE - Proper OpenGL cleanup
int WinDestroy(int window_id) {
// Clean up textures
if (font_texture) {
glDeleteTextures(1, &font_texture);
}
// Clean up fonts
for (int i = 0; i < MAX_FONT_STYLES; i++) {
if (fonts[i].loaded && fonts[i].texture_id) {
glDeleteTextures(1, &fonts[i].texture_id);
}
}
return 0;
}
Event System Implementation
X11 Event System (Original)
int EventPoll() {
state.event_type = 0;
if (XPending(state.display)) {
XNextEvent(state.display, &state.event);
switch (state.event.type) {
case KeyPress:
state.event_type = EVENT_KEY_PRESS;
state.key_code = XLookupKeysym(&state.event.xkey, 0);
// Character handling with XIM/XIC
break;
case ButtonPress:
state.event_type = EVENT_MOUSE_PRESS;
state.mouse_x = state.event.xbutton.x;
state.mouse_y = state.event.xbutton.y;
break;
}
}
return state.event_type;
}
GLFW Event System (Modern)
// Global event state
static struct {
int event_type;
int key_code;
char key_char;
double mouse_x, mouse_y;
int mouse_button;
} state;
// Callbacks set the event state
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
state.event_type = EVENT_KEY_PRESS;
state.key_code = key;
// Handle character conversion
if (key >= 32 && key <= 126) {
state.key_char = (char)key;
if (key >= 'A' && key <= 'Z' && !(mods & GLFW_MOD_SHIFT)) {
state.key_char = key + 32; // Lowercase
}
}
}
}
// Polling function returns callback-set events
int EventPoll() {
int current_event = state.event_type;
state.event_type = EVENT_NONE;
glfwPollEvents(); // Triggers callbacks
if (glfwWindowShouldClose(state.window)) {
return EVENT_CLOSE;
}
return (state.event_type != EVENT_NONE) ? state.event_type : EVENT_NONE;
}
Event Binding Pattern
# Constants for event types
alias EVENT_NONE = 0
alias EVENT_CLOSE = 10
alias EVENT_KEY_PRESS = 2
alias EVENT_MOUSE_PRESS = 4
# Main polling function
fn EventPoll(self) raises -> Int32:
var f = self.handle.get_function[fn() -> Int32]("EventPoll")
return f()
# Event data getters
fn EventGetKey(self) raises -> Int32:
var f = self.handle.get_function[fn() -> Int32]("EventGetKey")
return f()
fn EventGetChar(self) raises -> Int32:
var f = self.handle.get_function[fn() -> Int32]("EventGetChar")
return f()
Advanced Binding Techniques
1. State Management Pattern
// Centralized state structure
static struct {
// Drawing state
float draw_color[4];
float draw_pos[2];
// Event state
int event_type;
int key_code;
// Resource state
GLuint font_texture;
FontFace fonts[MAX_FONT_STYLES];
} state = {0}; // Zero-initialize everything
2. ID-Based Resource Management
// Resources accessed by ID - safe and simple
int TextEditInit(int edit_id, int max_length) {
if (edit_id < 0 || edit_id >= MAX_TEXT_EDITORS) return -1;
// Initialize text editor with ID
}
int FontLoad(int font_id, const char* font_name, float size) {
if (font_id < 0 || font_id >= MAX_FONT_STYLES) return -1;
// Load font with ID
}
Mojo Usage:
# ID-based resource management
_ = gui.TextEditInit(0, 100) # Text editor 0
_ = gui.TextEditInit(1, 50) # Text editor 1
_ = gui.FontLoad(FONT_STYLE_BOLD, "Bold", 16.0) # Font style 1
3. Callback Data Handling
// Store callback data in global state
static void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) {
if (action == GLFW_PRESS) {
state.event_type = EVENT_MOUSE_PRESS;
state.mouse_button = button + 1; // Convert to 1-based
glfwGetCursorPos(window, &state.mouse_x, &state.mouse_y);
}
}
// Access via simple getters
int EventGetMouseX() { return (int)state.mouse_x; }
int EventGetMouseY() { return (int)state.mouse_y; }
4. Complex Widget State
// Text editing state
typedef struct {
char text[MAX_STRING_LEN];
int cursor_pos;
int selection_start;
int selection_end;
int max_length;
} TextEditState;
static TextEditState text_edits[MAX_TEXT_EDITORS];
// Simple interface functions
int TextEditInsertChar(int edit_id, int ch) {
if (edit_id < 0 || edit_id >= MAX_TEXT_EDITORS) return -1;
TextEditState *edit = &text_edits[edit_id];
// Insert character logic
}
Performance Optimizations
1. Minimal FFI Calls
# β INEFFICIENT - Multiple FFI calls per frame
for i in range(1000):
_ = gui.DrawSetColor(1.0, 0.0, 0.0, 1.0)
_ = gui.DrawSetPos(float(i), 100.0)
_ = gui.DrawRect(10.0, 10.0)
# β
EFFICIENT - Batch operations
_ = gui.DrawSetColor(1.0, 0.0, 0.0, 1.0)
for i in range(1000):
_ = gui.DrawSetPos(float(i), 100.0)
_ = gui.DrawRect(10.0, 10.0)
2. String Caching
# β
EFFICIENT - Register strings once, reuse IDs
register_string(gui, "Static Text", 10) # Register once
# In render loop - just use ID
_ = gui.DrawText(10, 16.0) # Fast ID-based rendering
3. State Minimization
// β
EFFICIENT - Only store essential state
typedef struct {
float position; // 0.0 to 1.0
float handle_size; // 0.0 to 1.0
int dragging; // Boolean flag
} ScrollbarState;
Common Pitfalls & Solutions
1. Memory Corruption
Problem: Buffer overflows and invalid memory access.
// β DANGEROUS - No bounds checking
void unsafe_string_copy(char* dest, const char* src) {
strcpy(dest, src); // Can overflow
}
// β
SAFE - Always check bounds
int safe_string_copy(char* dest, const char* src, int max_len) {
if (!dest || !src || max_len <= 0) return -1;
strncpy(dest, src, max_len - 1);
dest[max_len - 1] = '\0'; // Always null-terminate
return 0;
}
2. Resource Leaks
Problem: OpenGL resources not cleaned up.
// β
PATTERN - Always pair allocation with cleanup
int create_texture() {
GLuint texture;
glGenTextures(1, &texture);
return texture;
}
void cleanup_texture(GLuint texture) {
if (texture) {
glDeleteTextures(1, &texture);
}
}
3. Event State Corruption
Problem: Event state overwritten between poll and use.
// β PROBLEMATIC - State can be overwritten
int EventPoll() {
state.event_type = EVENT_NONE; // Clears before checking callbacks
glfwPollEvents();
return state.event_type;
}
// β
FIXED - Preserve state properly
int EventPoll() {
int current_event = state.event_type;
state.event_type = EVENT_NONE;
glfwPollEvents();
return (state.event_type != EVENT_NONE) ? state.event_type : EVENT_NONE;
}
4. Type Size Mismatches
Problem: C types donβt match Mojo assumptions.
// β
EXPLICIT - Use exact-size types
#include <stdint.h>
int32_t GetMouseX(); // Explicit 32-bit
float GetMouseXf(); // Explicit float type
# β
MATCHING - Use corresponding Mojo types
fn EventGetMouseX(self) raises -> Int32: # Matches int32_t
fn EventGetMouseXf(self) raises -> Float32: # Matches float
Complete Example: Text Input System
This example shows all techniques combined:
C Implementation:
// State management
typedef struct {
char text[MAX_STRING_LEN];
int cursor_pos;
int max_length;
} TextEditState;
static TextEditState text_edits[MAX_TEXT_EDITORS];
// Simple interface
int TextEditInit(int edit_id, int max_length) {
if (edit_id < 0 || edit_id >= MAX_TEXT_EDITORS) return -1;
memset(&text_edits[edit_id], 0, sizeof(TextEditState));
text_edits[edit_id].max_length = max_length;
return 0;
}
int TextEditInsertChar(int edit_id, int ch) {
if (edit_id < 0 || edit_id >= MAX_TEXT_EDITORS) return -1;
TextEditState *edit = &text_edits[edit_id];
int len = strlen(edit->text);
if (len >= edit->max_length - 1) return -1;
// Safe character insertion with bounds checking
memmove(&edit->text[edit->cursor_pos + 1],
&edit->text[edit->cursor_pos],
len - edit->cursor_pos + 1);
edit->text[edit->cursor_pos] = (char)ch;
edit->cursor_pos++;
return 0;
}
Mojo Binding:
fn TextEditInit(self, edit_id: Int32, max_length: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32, Int32) -> Int32]("TextEditInit")
return f(edit_id, max_length)
fn TextEditInsertChar(self, edit_id: Int32, ch: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32, Int32) -> Int32]("TextEditInsertChar")
return f(edit_id, ch)
Usage Pattern:
# Initialize text editor
_ = gui.TextEditInit(0, 100)
# Handle keyboard input
if event == EVENT_KEY_PRESS:
var ch = gui.EventGetChar()
if ch >= 32 and ch <= 126: # Printable character
_ = gui.TextEditInsertChar(0, ch)
Key Success Principles
- Keep It Simple - Avoid complex data structures in FFI
- One Function, One Purpose - Each C function does exactly one thing
- ID-Based Resources - Use integer IDs instead of pointers
- Bounds Check Everything - Always validate parameters
- Clean State Management - Use global state with clear ownership
- Pair Operations - Every allocation has corresponding cleanup
- Error Codes - Return integers, not exceptions
- Cache Strings - Register once, reference by ID
These patterns solved all major FFI issues and created a robust, production-ready GUI system for Mojo!
This guide represents months of FFI problem-solving condensed into reusable patterns. Use these techniques for any Mojo FFI project!
Part 2
Mojo FFI Patterns - The Definitive Guide
Introduction
This document contains the EXACT FFI patterns that work in Mojo. These patterns were discovered through extensive experimentation and represent the first successful GUI implementation in Mojo.
Core FFI Pattern - DLHandle
The Golden Pattern
struct MyWrapper:
var handle: DLHandle
fn __init__(inout self) raises:
self.handle = DLHandle("./mylib.so")
fn my_function(self, param: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32) -> Int32]("my_function")
return f(param)
Key Rules
- Always use DLHandle - Donβt use external_call
- Store handle as member variable - Donβt create it each call
- Simple types only - Int32, Float32, Float64, etc.
Working Function Signatures
Simple Types - ALWAYS WORK
# Integer parameters and returns
fn set_value(self, value: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32) -> Int32]("set_value")
return f(value)
# Float parameters
fn set_position(self, x: Float32, y: Float32) raises -> Int32:
var f = self.handle.get_function[fn(Float32, Float32) -> Int32]("set_position")
return f(x, y)
# Mixed types
fn draw_circle(self, x: Float32, y: Float32, radius: Float32, segments: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Float32, Float32, Float32, Int32) -> Int32]("draw_circle")
return f(x, y, radius, segments)
# No parameters
fn init(self) raises -> Int32:
var f = self.handle.get_function[fn() -> Int32]("init")
return f()
Complex Types - AVOID
# DON'T DO THIS - Arrays cause crashes
fn set_matrix(self, matrix: List[Float32]) raises -> Int32: # WRONG!
# This will crash or not work properly
# DON'T DO THIS - Strings are problematic
fn set_text(self, text: String) raises -> Int32: # RISKY!
# String passing is complex, see workarounds below
# DON'T DO THIS - Structs don't map
fn set_rect(self, rect: MyRect) raises -> Int32: # DOESN'T WORK!
# Mojo structs don't map to C structs
String Handling Patterns
Pattern 1: Character-by-Character Building
# Mojo side
fn register_string(gui: MyWrapper, text: String, string_id: Int32) raises:
_ = gui.str_clear(string_id)
for i in range(len(text)):
_ = gui.str_register(ord(text[i]), i, string_id)
# C side
int str_register(int ch, int position, int string_id) {
if (string_id < 0 || string_id >= MAX_STRINGS) return -1;
state.strings[string_id][position] = (char)ch;
state.strings[string_id][position + 1] = '\0';
return 0;
}
int str_clear(int string_id) {
if (string_id < 0 || string_id >= MAX_STRINGS) return -1;
memset(state.strings[string_id], 0, MAX_STRING_LEN);
return 0;
}
Pattern 2: Direct String Passing (When Necessary)
# Mojo side - using unsafe_ptr()
fn draw_text_direct(self, text: String, size: Float32) raises -> Int32:
var f = self.handle.get_function[fn(UnsafePointer[Int8], Float32) -> Int32]("draw_text_direct")
return f(text.unsafe_ptr(), size)
# C side - no arrays!
int draw_text_direct(const char* text, float size) {
if (!text) return -1;
// Use text directly, don't store in array
DrawText(text, strlen(text), size);
return 0;
}
Array/Matrix Handling Patterns
Pattern: Element-by-Element Building
# Mojo side - Set matrix values individually
fn set_projection_matrix(gui: MyWrapper, width: Float32, height: Float32) raises:
var matrix_id = 0
_ = gui.matrix_clear(matrix_id)
# Set each element
_ = gui.matrix_set_value(matrix_id, 0, 0, 2.0/width)
_ = gui.matrix_set_value(matrix_id, 1, 1, -2.0/height)
_ = gui.matrix_set_value(matrix_id, 2, 2, -1.0)
_ = gui.matrix_set_value(matrix_id, 3, 3, 1.0)
_ = gui.matrix_apply(matrix_id)
# C side - Store internally, never expose
static float matrices[16][16]; // Internal storage
int matrix_clear(int matrix_id) {
if (matrix_id < 0 || matrix_id >= 16) return -1;
memset(matrices[matrix_id], 0, 16 * sizeof(float));
return 0;
}
int matrix_set_value(int matrix_id, int row, int col, float value) {
if (matrix_id < 0 || matrix_id >= 16) return -1;
if (row < 0 || row >= 4 || col < 0 || col >= 4) return -1;
matrices[matrix_id][row * 4 + col] = value;
return 0;
}
int matrix_apply(int matrix_id) {
if (matrix_id < 0 || matrix_id >= 16) return -1;
glUniformMatrix4fv(projection_loc, 1, GL_FALSE, matrices[matrix_id]);
return 0;
}
Common Pitfalls and Solutions
Pitfall 1: Using external_call
# WRONG - This often doesn't link properly
@external_call
fn my_function(x: Int32) -> Int32: ...
# RIGHT - Use DLHandle
fn my_function(self, x: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Int32) -> Int32]("my_function")
return f(x)
Pitfall 2: Passing Mojo Collections
# WRONG - List doesn't map to C array
fn set_values(self, values: List[Float32]) raises:
# This won't work!
# RIGHT - Pass elements individually
fn set_values(self, values: List[Float32]) raises:
for i in range(len(values)):
_ = self.set_value_at(i, values[i])
Pitfall 3: Complex Return Types
# WRONG - Can't return structs or arrays
fn get_rect(self) raises -> Rect: # Doesn't work!
# RIGHT - Return components separately
fn get_rect_x(self) raises -> Float32:
var f = self.handle.get_function[fn() -> Float32]("get_rect_x")
return f()
fn get_rect_y(self) raises -> Float32:
var f = self.handle.get_function[fn() -> Float32]("get_rect_y")
return f()
Template for New C Functions
Step 1: Design C Function with Simple Types
// In your_library.c
int my_new_function(float param1, int param2) {
// Implementation
return 0; // Return error code
}
// For data that would normally be arrays
int set_data_element(int index, float value) {
if (index < 0 || index >= MAX_SIZE) return -1;
internal_data[index] = value;
return 0;
}
Step 2: Create Mojo Binding
# In your_wrapper.mojo
fn my_new_function(self, param1: Float32, param2: Int32) raises -> Int32:
var f = self.handle.get_function[fn(Float32, Int32) -> Int32]("my_new_function")
return f(param1, param2)
fn set_data_element(self, index: Int32, value: Float32) raises -> Int32:
var f = self.handle.get_function[fn(Int32, Float32) -> Int32]("set_data_element")
return f(index, value)
Step 3: Use in Mojo
fn main() raises:
var wrapper = MyWrapper()
# Simple function call
_ = wrapper.my_new_function(3.14, 42)
# Array-like data
var data = List[Float32](1.0, 2.0, 3.0, 4.0)
for i in range(len(data)):
_ = wrapper.set_data_element(i, data[i])
Resource Management Pattern
ID-Based Resources
# Create resources with IDs
var window_id = gui.window_create(800, 600)
var texture_id = gui.texture_create(256, 256)
var shader_id = gui.shader_create()
# Use resources by ID
_ = gui.window_activate(window_id)
_ = gui.texture_bind(texture_id)
_ = gui.shader_use(shader_id)
# Destroy resources by ID
_ = gui.window_destroy(window_id)
_ = gui.texture_destroy(texture_id)
_ = gui.shader_destroy(shader_id)
Best Practices Summary
- Keep It Simple - If a function signature looks complex, redesign it
- Use IDs - Donβt pass objects, pass integer IDs
- Build Incrementally - Donβt pass arrays, build them element by element
- State in C - Keep complex state on the C side
- Simple Returns - Only return simple types (int, float)
- Error Codes - Return 0 for success, -1 for error
- Validate Everything - Check bounds on the C side
Testing Your FFI
fn test_ffi() raises:
var wrapper = MyWrapper()
# Test simple function
var result = wrapper.simple_function(42)
if result != 0:
print("FFI test failed!")
return
# Test string handling
register_string(wrapper, "Hello, Mojo!", 0)
# Test array handling
for i in range(4):
for j in range(4):
_ = wrapper.matrix_set_value(0, i, j, 0.0)
print("FFI tests passed!")
Conclusion
These patterns represent hard-won knowledge from creating the first working GUI framework in Mojo. By following these patterns exactly, you can successfully interface Mojo with C libraries and create powerful applications.
Remember: When in doubt, make it simpler!