Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction to NextStd

Welcome to the official documentation for NextStd—the safer, modern alternative to the traditional C standard library.

For decades, C developers have relied on <stdio.h>, <string.h>, and <stdlib.h>. While incredibly fast, these legacy libraries are fundamentally unsafe. A single mismatched %d in a printf, a missing \0 in a string, or a silent NULL pointer can trigger catastrophic Segmentation Faults, buffer overflows, and security vulnerabilities.

NextStd fixes this at the foundation.

By combining the elegant simplicity of C11 macros on the frontend with the mathematically proven memory safety of Rust on the backend, NextStd delivers a zero-compromise development experience.

Core Pillars of NextStd

  • Type-Safe I/O: Never write a format specifier again. NextStd uses C11 _Generic macros to automatically route data types at compile time.
  • Crash-Proof Control Flow: Replace silent failures and dangerous goto statements with Python-style NS_TRY and NS_EXCEPT macros.
  • Immunity to NULL: Every FFI boundary rigorously checks for NULL pointers. Passing bad memory to NextStd gracefully returns an error code instead of killing your program.
  • Modern String Memory: Strings are powered by Small String Optimization (SSO), meaning short text costs zero heap allocations, and massive text safely catches Out-Of-Memory (OOM) errors without panicking.

Who is this for?

NextStd is built for C developers, systems programmers, and embedded engineers who want the safety guarantees of a modern language without actually having to rewrite their entire codebase in Rust. It compiles down to a standard static or dynamic C library (.a, .so, .dylib, or .dll) and links identically t o any standard C dependency.

Ready to upgrade your C code?

Navigate through this book using the sidebar to the left. We recommend starting with Why NextStd? for a deeper dive into the architectural philosophy, or jumping straight into Getting Started to write your first safe program!

Why NextStd?

C is the undisputed language of the physical and systems world. Whether you are building real-time camera processing pipelines, writing constrained firmware for microcontrollers like the ESP32 and RP2040, or crafting lightning-fast terminal applications, C gives you the raw, uncompromised control you need.

But that control comes with a massive, decades-old hidden cost: The Standard Library.

The Legacy C Trap

Libraries like <stdio.h>, <string.h>, and <stdlib.h> were designed in an era before cybersecurity was a primary concern. Today, they are the root cause of the majority of software vulnerabilities.

Consider a standard C input operation:

int age;
printf("Enter your age: ");
scanf("%d", &age);

If a user types "twenty" instead of 20, scanf silently fails, leaves the invalid text in the input buffer (corrupting future reads), and leaves age uninitialized. If you try to concatenate two strings using strcat and miscalculate the buffer size by a single byte, you trigger a buffer overflow. If you pass a NULL pointer into strlen, your entire system crashes with a Segmentation Fault.

In mission-critical environments, a single uncaught NULL pointer can brick a device or crash a production system.

The Rewrite Dilemma

The modern industry answer to C’s unsafety is to rewrite everything in Rust.

Rust is incredible. It mathematically proves memory safety at compile time. However, rewriting massive, established C codebases, or trying to interface Rust with highly specific hardware vendor SDKs, is often unrealistic, expensive, and time-consuming.

Developers shouldn’t have to abandon their C codebases just to get modern safety guarantees.

The NextStd Bridge

NextStd was built to be the bridge. It is a drop-in systems library that brings the fearless safety of Rust directly into C, using standard Foreign Function Interface (FFI) boundaries.

With NextStd, you don’t rewrite your C code; you just upgrade your tools.

  • You keep your C compiler, your Makefiles, and your existing architecture.
  • You replace dangerous functions like printf and scanf with ns_print and ns_read.
  • You gain compile-time type routing, NULL pointer immunity, safe heap allocation, and Python-style TRY/EXCEPT error handling.

By handling the most dangerous operations (I/O, string manipulation, and dynamic memory) in a memory-safe backend, NextStd allows you to write C code that is fundamentally immune to standard library footguns.

You get the speed of C, with the peace of mind of Rust.

Safety Guarantees

When we say NextStd is “safe,” we aren’t just talking about compiler warnings or best practices. We are talking about architectural guarantees enforced at the Foreign Function Interface (FFI) boundary between C and Rust.

Here are the core vulnerability classes that NextStd entirely eliminates from your C codebase.

1. Null Pointer Immunity (No More Segfaults)

In standard C, passing a NULL pointer to strlen() or strcpy() results in immediate Undefined Behavior, typically manifesting as a fatal Segmentation Fault.

NextStd actively intercepts bad memory. Every single FFI boundary function in the library checks for NULL before performing any operations. If a NULL pointer is detected, the operation is safely aborted, and an NS_ERROR_ANY code is returned for your NS_TRY block to catch gracefully.

2. Out-of-Memory (OOM) Protection

Standard C’s malloc returns NULL when the heap is exhausted—a condition developers frequently forget to check. Standard Rust, on the other hand, will panic and instantly crash the program when an allocation fails.

NextStd takes the safe middle path. It uses Rust’s try_reserve API for all dynamic heap allocations. If the system runs out of memory (a critical threat in embedded and constrained environments), NextStd safely catches the failure and returns an NS_ERROR_STRING_ALLOC code without crashing your process.

3. Buffer Overflow Prevention

C strings are notoriously dangerous null-terminated (\0) arrays. If you forget the terminator, or use strcat into a buffer that is even one byte too small, you silently overwrite adjacent memory.

NextStd uses a length-prefixed struct combined with Small String Optimization (SSO). The exact length of the string is always known. If a string needs to grow beyond its 24-byte inline capacity, NextStd automatically and safely requests exactly the right amount of heap memory. Buffer overflows are mathematically impossible by design.

4. Format String Vulnerabilities

Using printf("%d", "text") forces the C compiler to trust the developer blindly. If the format specifier doesn’t match the variable type, the program reads garbage memory from the stack or crashes entirely.

NextStd leverages C11 _Generic macros to completely eliminate this class of bugs by routing types at compile time:

int age = 21;
ns_string name; // Assume initialized

ns_println(age);  // Automatically routes to ns_print_int
ns_println(name); // Automatically routes to ns_print_string

Because there are no format strings, format string vulnerabilities simply cannot exist.

Getting Started with NextStd

Welcome to the practical side of NextStd. Up to this point, we have covered the philosophy and the safety guarantees. Now, it is time to actually write some code.

Integrating a Rust-backed static or dynamic library into a C project might sound intimidating, but standard tooling makes the process incredibly straightforward. Because NextStd exposes a standard Foreign Function Interface (FFI) boundary, your C compiler treats it exactly like any other legacy C library.

Note: Alpha Stage Development > Because NextStd is currently in active Alpha, standalone integration (moving the headers and .a files to a completely separate C project) requires manual linker configuration. For the smoothest experience right now, we highly recommend working directly inside the cloned NextStd project directory and utilizing the provided examples/ folder and Makefile!

The Integration Workflow

Working within the NextStd repository follows a simple workflow:

  1. Build the Backend: First, compile the Rust source code into a static archive (.a). We’ve wrapped Cargo in our Makefile, so you just need to run:

    make rust
    
  2. Write Your C Code: Create a new .c file inside the examples/ directory. Since you are in the project folder, you can simply include the core headers using relative paths:

    #include "../include/ns.h"
    #include "../include/ns_error.h"
    
  3. Link and Compile: Instead of running raw gcc commands and manually linking the Rust archive, use the built-in Makefile. You can view all available example targets by running:

    make list
    

    (To build and run a specific example, just type make followed by the filename without the .c extension, e.g., make 01_hello_world!)

Prerequisites

Before moving forward with the installation, ensure your development environment has the required tools installed:

  • A C Compiler: gcc or clang (or a cross-compiler if you are targeting embedded systems).
  • The Rust Toolchain: You need cargo and rustc installed to build the backend. (Available via rustup.rs).
  • Make: To run the build scripts that seamlessly tie the Rust backend and C frontend together.

What’s Next?

Use the sidebar to navigate through the setup process:

  • Installation & Linking: Step-by-step instructions for compiling the backend and connecting it to your C project.
  • Your First Program: Write, compile, and run your first memory-safe NextStd application.
  • Build System Integration: Learn how to seamlessly integrate NextStd into your Makefile or CMakeLists.txt for automated builds.

Installation & Linking

NextStd is designed to be installed globally on your Unix-like system, allowing you to include its memory-safe data structures and I/O functions in any C project just like the standard library.

Here is how to get your environment set up in under two minutes.

Prerequisites

Before installing, ensure your system has the required build tools:

  1. Rust & Cargo: To compile the safe back end. Run the official installer in your terminal:

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

    👉 Note for beginners: The script will pause. Press 1 and hit Enter to proceed with the default installation.

    Important

    Once the installation finishes, you must tell your current terminal where Rust is installed. Run this command:

    source $HOME/.cargo/env
    

    (Alternatively, you can just close your terminal and open a new one).

  2. GCC or Clang: For the C front-end and macro expansion.

  3. Make: For build automation.


Step 1: Clone the Repository

Grab the latest version of the NextStd source code from GitHub:

git clone https://github.com/NextStd/nextstd.git
cd nextstd

To make NextStd available to all your C projects, you need to build the optimized Rust binaries and install them globally.

First, build the core libraries (this runs safely as your normal user):

make rust

Then, install the headers and static archives into your system’s standard /usr/local directories (this requires root privileges to copy the files):

sudo make install

What is happening behind the scenes?

  • The C headers (ns.h, ns_data.h, etc.) are securely placed in /usr/local/include/nextstd/.
  • The compiled Rust static archives (libns_io.a, libns_data.a, etc.) are placed in /usr/local/lib/.

(To completely remove the library from your system later, simply run sudo make uninstall from the repository root).


Step 3: Linking in Your Projects

Now that NextStd is installed globally, you can include it in any standalone C file across your machine using standard angle brackets:

#include <nextstd/ns.h>
#include <nextstd/ns_data.h>

When compiling your standalone program, you need to tell GCC to link the specific NextStd modules you are using, as well as the standard system libraries that the Rust standard library depends on.

Because the library is in /usr/local/lib, you no longer need complex -L paths. A typical compilation command looks like this:

gcc main.c -lns_data -lns_io -lns_string -lns_error -lpthread -ldl -lm -o my_safe_program
  • -lns_*: Links your modular NextStd archives.
  • -lpthread, -ldl, -lm: Standard system libraries required by the Rust backend on standard Linux/macOS environments.

Alternative: Local Testing

If you want to test the library, develop new modules, or run the built-in examples without installing it system-wide, you can build the backend locally without sudo:

make rust

Then, run one of the pre-configured examples (do not include the .c extension):

To list all the examples use:

make list

And then you can use the example names as given below.

Note

Note the absence of the .c extension

make 01_print_integer

This commands your compiler to grab the example, link it against your local target/release/ directory, and execute the resulting binary.

Your First Program

Now that your environment is set up and NextStd is installed globally on your system, let’s write your first memory-safe C program.

We are going to replace the traditional, unsafe printf function with NextStd’s type-safe ns_println macro.

The Basic Program

Create a new file named hello.c anywhere on your computer.

Add the following code to print basic text and variables. Notice how we use standard angle brackets for the includes now that NextStd is a system library:

#include <nextstd/ns.h>

int main() {
    // 1. Printing a standard string
    ns_println("Hello, World! Welcome to NextStd.");

    // 2. Printing an integer safely without format specifiers
    int version = 2;
    ns_print("NextStd Version: ");
    ns_println(version); 

    // 3. Printing a floating-point number
    double pi = 3.14159;
    ns_print("Value of Pi: ");
    ns_println(pi);

    return 0;
}

To compile and execute this program, open your terminal and link the necessary NextStd I/O and Error modules:

gcc hello.c -lns_io -lns_error -o hello
./hello

(Note: If you are testing locally inside the cloned repository instead of using the system-wide install, you can still just drop this in examples/hello.c and run make hello)

Adding Terminal Colors

NextStd also provides a dedicated, cross-platform color module to help you build beautiful CLI tools. You don’t need to remember ANSI escape codes; you just include the header and use the macros.

Create a second file named hello_color.c:

#include <nextstd/ns.h>
#include <nextstd/ns_color.h> // Import the color macros

int main() {
    // Printing with a specific color and resetting it afterward
    ns_println(NS_COLOR_GREEN "Success: System initialized safely." NS_COLOR_RESET);

    // Combining styles like bold text with colors
    ns_println(NS_COLOR_CYAN NS_COLOR_BOLD "NextStd is running..." NS_COLOR_RESET);

    // Warning and Error colors
    ns_println(NS_COLOR_YELLOW "Warning: Low memory." NS_COLOR_RESET);
    ns_println(NS_COLOR_RED "Error: Connection lost." NS_COLOR_RESET);

    return 0;
}

Compile and run this exactly like the first one:

gcc hello_color.c -lns_io -lns_error -o hello_color
./hello_color

How It Works

If you are coming from standard C, the code above might look like magic. How does ns_println know whether to print a string, an integer, or a double without you typing %s, %d, or %f?

NextStd leverages C11’s _Generic keyword to inspect the type of the variable at compile time. It then automatically routes your data to the correct, memory-safe Rust backend function (e.g., ns_print_int, ns_print_double, or ns_print_str).

Because there are no format strings, there is zero risk of mismatched types causing undefined behavior or reading garbage memory from the stack. If you try to print a type that NextStd doesn’t support yet, the compiler will safely throw an error before the program ever runs.

Build System Integration

Because NextStd installs its headers and compiled archives directly into your system’s standard /usr/local/ directories, integrating it into your own projects is as simple as linking any other C library.

The only special requirement is that because NextStd’s backend is powered by Rust, you must link a few standard system libraries (pthread, dl, and m) alongside the NextStd modules.

Here is how to configure the most popular build systems and command runners to work with NextStd.

GNU Make (Makefile)

If you are using a standard Makefile, you just need to append the NextStd libraries to your LDFLAGS (Linker Flags) variable.

Here is a minimal, robust Makefile for a NextStd project:

CC = gcc
CFLAGS = -Wall -Wextra -O2
# Link your required NextStd modules and the Rust system dependencies
LDFLAGS = -lns_data -lns_io -lns_string -lns_error -lpthread -ldl -lm

TARGET = my_app
SRC = main.c

all: $(TARGET)

$(TARGET): $(SRC)
 $(CC) $(CFLAGS) $(SRC) -o $(TARGET) $(LDFLAGS)

clean:
 rm -f $(TARGET)

Tip: Order matters in GCC! Always put your source files (main.c) before your -l linker flags, or the compiler might complain about undefined references.

Just (justfile)

If you prefer using just (a modern, handy command runner highly popular in the Rust ecosystem), configuring it is incredibly clean.

Here is a ready-to-use justfile that handles building, running, and cleaning your NextStd project:

compiler := "gcc"
cflags := "-Wall -Wextra -O2"
libs := "-lns_data -lns_io -lns_string -lns_error -lpthread -ldl -lm"
target := "my_app"
src := "main.c"

# Default recipe: Build the application
@build:
    {{compiler}} {{cflags}} {{src}} -o {{target}} {{libs}}

# Build and immediately run the application
@run: build
    ./{{target}}

# Clean up compiled artifacts
@clean:
    rm -f {{target}}

To build and run your safe C code, you simply type just run in your terminal!

CMake (CMakeLists.txt)

If you are using CMake, the process is just as easy. You use the target_link_libraries directive to tell CMake exactly what needs to be bundled into your executable.

Here is a complete CMakeLists.txt file:

cmake_minimum_required(VERSION 3.10)

# Define the project and enable C
project(NextStdApp C)

# Add your executable
add_executable(my_app main.c)

# Link the NextStd archives and the required system libraries
target_link_libraries(my_app
    ns_data
    ns_io
    ns_string
    ns_error
    pthread
    dl
    m
)

In the examples above, we linked all four core NextStd modules (ns_data, ns_io, ns_string, ns_error).

NextStd is highly modular. If you are writing a small script that only uses ns_print and doesn’t use vectors, hashmaps, or strings, you can safely omit -lns_data and -lns_string from your build configuration to slightly reduce your final binary size.

However, -lns_error is required by almost all modules, so it should generally always be included.

Error Handling (ns_error)

In standard C, error handling is historically fragmented and highly error-prone. Some functions return -1, others return NULL, some set errno as a global state, and others simply crash the program if given invalid input.

This inconsistency makes it incredibly easy to forget to check an error condition, leading to undefined behavior, silent failures, or segmentation faults.

NextStd takes a modern approach. It unifies all error reporting into a single, predictable type: ns_error_t.

Every NextStd operation that allocates memory, parses input, or interacts with the Rust backend securely returns an ns_error_t code instead of returning naked pointers or relying on global error states.

What’s in this chapter?

This chapter covers how NextStd handles errors safely and elegantly, allowing you to write robust C code without the usual boilerplate:

  • The Try/Except Macros: Learn how to use NS_TRY and NS_EXCEPT to write clean, structured error handling that visually separates your “happy path” from your failure logic.
  • Null Pointer Protection: Discover how the Rust FFI boundary safely intercepts and neutralizes NULL pointers before they can cause a system-level segfault.
  • Error Codes Reference: A complete reference guide for all ns_error_t values (such as NS_SUCCESS and NS_ERROR_ANY) and how to handle them.

By enforcing a standard error type and structured control flow across the entire library, NextStd ensures that your programs stay stable, predictable, and memory-safe.

The Try/Except Macros

Standard C doesn’t have try and catch blocks. Instead, developers are forced to write endless if (result != 0) checks, which clutters the “happy path” of the code and makes the logic hard to read.

NextStd solves this by providing the NS_TRY and NS_EXCEPT macros. These give you the clean, readable structure of modern exception handling without any performance overhead—it all compiles down to standard C if/else statements under the hood.

The Basic Syntax

To use the macros, you first declare an ns_error_t variable to hold the return state. Then, you pass that variable and the function you want to execute into NS_TRY.

Here is a standard example of safely initializing an ns_string (Note: We will cover strings in depth in the Strings chapter, but they make a perfect example here!):

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_string my_text;

    // Try to safely allocate memory for the string
    NS_TRY(err, ns_string_new(&my_text, "Hello, memory-safe world!")) {
        // This block ONLY runs if the function returns NS_SUCCESS
        ns_println("String initialized successfully:");
        ns_println(my_text);
    } 
    NS_EXCEPT(err, NS_ERROR_ANY) {
        // This block runs if an error occurred (e.g., out of memory)
        ns_println("Critical failure: Could not allocate string.");
        return 1;
    }

    ns_string_free(&my_text);
    return 0;
}

How It Works

NS_TRY evaluates the function you pass to it. If the function returns NS_SUCCESS (which equals 0), the code inside the {} brackets executes.

If the function returns anything else, it skips the NS_TRY block and evaluates the NS_EXCEPT block. The err variable stores the exact error code returned by the function, so you can inspect it or log it inside the exception block.

The “Macro Comma” Gotcha

As you progress to advanced NextStd features (like Vectors and HashMaps later in this book), you might run into a specific C preprocessor quirk.

If the function you are passing into NS_TRY is itself a macro containing commas (such as ns_map_at), the C compiler will get confused. It sees the commas inside your function and mistakenly thinks you are passing too many arguments to NS_TRY.

The Problem:

// COMPILER ERROR: Too few arguments to function call
NS_TRY(err, ns_map_at(&map, ns_string, key, &val)) { ... }

The Solution: Wrap the inner macro call in an extra set of parentheses. This “hides” the commas from the outer NS_TRY macro, allowing the preprocessor to expand it correctly.

// SUCCESS: Notice the extra parentheses around the map function
NS_TRY(err, (ns_map_at(&map, ns_string, key, &val))) { 
    ns_print("Value retrieved successfully.");
} NS_EXCEPT(err, NS_ERROR_ANY) {
    ns_println("Key not found in map.");
}

Alternatively, you can evaluate the macro first, store its result in your error variable, and pass that variable into the try block:

err = ns_map_at(&map, ns_string, key, &val);
NS_TRY(err, err) {
    ns_print("Value retrieved successfully.");
}

Null Pointer Protection

In standard C, passing a NULL pointer into a standard library function (like strcpy or strlen) is a fatal mistake. The library will blindly attempt to read or write to memory address 0x0, resulting in an immediate Segmentation Fault and a hard crash of your entire application.

NextStd takes a drastically different approach to memory safety by leveraging its Rust backend as a protective shield.

The FFI Shield

Because NextStd’s core logic is executed in Rust, every pointer passed from your C code must cross the Foreign Function Interface (FFI) boundary.

Before NextStd ever attempts to read or write to a pointer you provide, the Rust backend actively verifies that the pointer is not null (using Rust’s .is_null() checks).

If a NULL pointer is detected, the Rust backend immediately halts the operation and returns a safe ns_error_t code (typically NS_ERROR_ANY or a specific null-pointer error code) back to C.

Graceful Failure in Action

Because NextStd intercepts the NULL pointer and translates it into an error code, your application continues running safely. You can then use your NS_TRY and NS_EXCEPT macros to handle the mistake gracefully.

Here is an example of attempting to initialize a string, but accidentally passing NULL instead of a valid memory address:

#include <nextstd/ns.h>
#include <stddef.h> // For NULL

int main() {
    ns_error_t err;

    // We maliciously pass NULL instead of a valid &ns_string pointer
    NS_TRY(err, ns_string_new(NULL, "This would normally crash!")) {

        ns_println("This block will never execute.");

    } 
    NS_EXCEPT(err, NS_ERROR_ANY) {

        // NextStd caught the NULL pointer and routed us here safely!
        ns_println("Crisis averted! NextStd refused to dereference the NULL pointer.");

    }

    return 0;
}

Why This Matters

By neutralizing NULL pointers at the library boundary, NextStd drastically reduces the debugging time spent hunting down mysterious segfaults. It transforms catastrophic system crashes into manageable, predictable error states that you can catch and log.

Error Codes Reference

Under the hood, ns_error_t is a strict enum defined by the Rust backend and exposed to C. Because it is a unified type, you can rely on these specific return codes across all NextStd modules.

Core Error Codes

When an operation fails, NextStd will return one of the following codes.

C Macro / Rust EnumValueDescription / Output
NS_SUCCESS0Success
NS_ERROR_ANY1Unknown Error. This acts as a catch-all (similar to Python’s except Exception).
NS_ERROR_IO_READ_FAILED10I/O Error: Failed to read input.
NS_ERROR_IO_WRITE_FAILED11I/O Error: Failed to write input.
NS_ERROR_INVALID_INPUT12I/O Error: Invalid Input format.
NS_ERROR_STRING_ALLOC_FAILED20String Error: Memory Allocation failed.
NS_ERROR_STRING_INVALID_UTF821String Error: Invalid UTF-8 sequence detected.
NS_ERROR_INDEX_OUT_OF_BOUNDS22Error: Index out of bounds.

Translating Errors to Strings (ns_error_message)

One of the most powerful features of NextStd’s error handling is the ns_error_message function.

Instead of forcing you to write massive switch statements in C to print the correct error, the Rust backend provides a helper function that translates the ns_error_t code directly into a readable C-string (const char*).

You can pass this directly into ns_println inside your exception blocks:

#include <nextstd/ns.h>
#include <nextstd/ns_data.h>

int main() {
    ns_error_t err;
    ns_vec my_list;

    // Initialize list with some data...
    ns_vec_new(&my_list, sizeof(int));

    int result;
    // Attempting to read an index that doesn't exist
    NS_TRY(err, (ns_vec_get(&my_list, int, 99, &result))) {
        ns_println("Successfully retrieved item!");
    } 
    NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_print("Operation failed: ");
        ns_println(ns_error_message(err)); 
    }

    ns_vec_free(&my_list);
    return 0;
}

By leveraging ns_error_message(err), your CLI applications can provide instantly readable, context-aware error messages to your users with zero extra C code.

Input & Output (ns_io)

In standard C, interacting with the terminal is a minefield.

Functions like printf and scanf rely on format strings (e.g., %d, %s, %f). If you accidentally mismatch the format specifier with the actual variable type, the compiler often won’t stop you. At best, you print garbage memory to the screen. At worst, you introduce severe security vulnerabilities (like Format String Attacks) or trigger buffer overflows that crash your entire system.

Furthermore, scanf is notorious for leaving dangling newlines in the input buffer, causing subsequent reads to randomly skip or fail.

The NextStd Approach

NextStd completely eliminates format strings from your codebase.

By leveraging C11’s powerful _Generic keyword, the ns_io module inspects your variables at compile time. It automatically routes your data to the correct, memory-safe Rust backend function.

If you try to print or read a data type that the library doesn’t support, your code simply won’t compile—catching the error before the program ever runs.

What’s in this chapter?

This chapter covers how to safely interact with the terminal without ever typing a % symbol again:

  • Safe Printing (ns_print): Learn how to output text and variables to the console seamlessly.
  • Safe Reading (ns_read): Discover how to take user input safely, without worrying about buffer overflows, invalid data types, or leftover newlines.
  • Terminal Colors: Make your CLI applications beautiful with cross-platform, macro-driven terminal colors and styles.

With ns_io, you get the convenience and safety of a high-level language’s print() and input() functions, running at the blistering speed of native C.

Safe Printing (ns_print)

In standard C, the printf function is inherently unsafe because it parses format strings (like %d or %s) at runtime. If the format string doesn’t perfectly match the variables you pass to it, the compiler might not catch it, leading to garbled output or severe security vulnerabilities.

NextStd replaces printf with two type-safe macros:

  • ns_print(val): Prints the value to the standard output without a newline.
  • ns_println(val): Prints the value and automatically appends a newline (\n).

Basic Usage

You never have to memorize format specifiers again. Simply pass your variable or literal directly into the macro, and NextStd will figure out how to print it.

#include <nextstd/ns.h>
#include <stdbool.h>

int main() {
    int age = 42;
    double pi = 3.14159;
    bool is_active = true;

    // Printing strings directly
    ns_println("--- User Profile ---");

    // Printing integers
    ns_print("Age: ");
    ns_println(age);

    // Printing floating-point numbers
    ns_print("Pi approximation: ");
    ns_println(pi);

    // Printing booleans (automatically formats as "true" or "false")
    ns_print("Account Active: ");
    ns_println(is_active);

    return 0;
}

How It Works Under the Hood

If C doesn’t support function overloading like C++ or Rust, how does ns_println know what type of data you are passing to it?

NextStd leverages a feature introduced in the C11 standard called _Generic selections. Think of it as a switch statement that evaluates data types at compile time rather than values at runtime.

When you call ns_print(val), the C preprocessor expands it into something like this:

// Simplified representation of the NextStd printing macro
#define ns_print(X) _Generic((X), \
    int: ns_print_int, \
    double: ns_print_double, \
    bool: ns_print_bool, \
    char*: ns_print_str, \
    const char*: ns_print_str \
)(X)

The Safety Guarantee

Because this evaluation happens strictly at compile time:

  1. Zero Runtime Overhead: There is no format string to parse while your program is running. It is as fast as calling the specific print function directly.
  2. Compile-Time Errors: If you try to pass a data type that NextStd doesn’t know how to print yet (like a raw struct or a multidimensional array), the compilation will fail immediately. It is impossible to accidentally trigger undefined behavior.

Safe Reading (ns_read)

Taking user input in standard C is notoriously difficult to get right.

If you use scanf("%s", buffer), you are immediately vulnerable to a buffer overflow if the user types more characters than your buffer can hold. Furthermore, scanf is famous for leaving dangling newline characters (\n) in the standard input stream, which causes subsequent read attempts to mysteriously skip or fail.

NextStd fixes all of this with a single, memory-safe macro: ns_read.

Basic Usage

Just like ns_print, ns_read uses C11 _Generic routing to figure out exactly what data type you are trying to read at compile time.

You simply pass a pointer (using the & address-of operator) to the variable you want to populate. Because interacting with the terminal can fail (e.g., the user types a word instead of a number), ns_read returns an ns_error_t that you should wrap in an NS_TRY block.

#include <nextstd/ns.h>

int main() {
  ns_error_t err;

  int age;
  ns_print("Enter your age: ");

  // Pass the memory address of 'age' so NextStd can populate it
  NS_TRY(err, ns_read(&age)) {
    ns_print("Success! You are ");
    ns_print(age);
    ns_println(" years old.");
  } 
  NS_EXCEPT(err, NS_ERROR_ANY) {
    ns_print("Failed to read input: ");
    ns_println(ns_error_message(err));
  }

  return 0;
}

How NextStd Protects You

When you call ns_read, the Rust backend takes complete control of the input stream and performs several safety checks before your C code ever sees the data:

  1. Type Validation: If you call ns_read(&age) (expecting an int) and the user types "hello", the Rust backend will safely reject the input and return NS_ERROR_INVALID_INPUT. Your program will not crash, and your age variable will remain untouched.
  2. Buffer Flushing: NextStd automatically flushes the standard input buffer after every read. You will never have to write hacky while ((c = getchar()) != '\n' && c != EOF) loops to clear out dangling newlines ever again.
  3. Memory Bounding: When reading strings (which we cover in the Strings chapter), the Rust backend dynamically allocates exactly the amount of memory needed to hold the user’s input. Buffer overflows are architecturally impossible.

Reading Multiple Types

Because ns_read infers the type from the pointer you pass it, you can reuse the exact same macro for integers, floats, and booleans.

#include <nextstd/ns.h>
#include <stdbool.h>

int main() {
  ns_error_t err;

  double price;
  ns_print("Enter the price: ");
  NS_TRY(err, ns_read(&price)) {
    ns_println(price);
  } NS_EXCEPT(err, NS_ERROR_ANY) {
    ns_println(ns_error_message(err));
  }

  bool is_student;
  ns_print("Are you a student? (true/false): ");
  NS_TRY(err, ns_read(&is_student)) {
    ns_println(is_student);
  } NS_EXCEPT(err, NS_ERROR_ANY) {
    ns_println(ns_error_message(err));
  }

  return 0;
}

Terminal Colors (ns_color)

When building a Command Line Interface (CLI), color is essential for user experience. It helps users instantly distinguish between a routine success message, a non-critical warning, and a fatal error.

In standard C, adding color means injecting raw ANSI escape codes directly into your strings, which makes your code incredibly difficult to read:

// Standard C: Hard to read and easy to mess up
printf("\033[1;31mError:\033[0m Could not open file.\n");

NextStd solves this by providing semantic, easy-to-read macros in the ns_color.h header.

Basic Usage

Because these macros resolve to standard string literals under the hood, you can rely on the C preprocessor to automatically concatenate them with your text.

Just include <nextstd/ns_color.h>, place the color macro immediately before your string, and always remember to append NS_COLOR_RESET at the end!

#include <nextstd/ns.h>
#include <nextstd/ns_color.h>

int main() {
    // A standard success message
    ns_println(NS_COLOR_GREEN "Success: User profile updated." NS_COLOR_RESET);

    // A warning message
    ns_println(NS_COLOR_YELLOW "Warning: Disk space is running low." NS_COLOR_RESET);

    // A critical error message
    ns_println(NS_COLOR_RED "Error: Failed to connect to the database." NS_COLOR_RESET);

    return 0;
}

Mixing Colors and Styles

You can stack multiple macros together to combine colors with text formatting, such as making a string bold or underlining a URL. Just separate them with a space.

#include <nextstd/ns.h>
#include <nextstd/ns_color.h>

int main() {
    // Bold and Cyan text
    ns_println(NS_COLOR_BOLD NS_COLOR_CYAN "Starting NextStd Server..." NS_COLOR_RESET);

    // Underlined Magenta text
    ns_println(NS_COLOR_UNDERLINE NS_COLOR_MAGENTA "https://github.com/NextStd/nextstd" NS_COLOR_RESET);

    return 0;
}

The Importance of Resetting

Terminal emulators are stateful. When you send a color code to the terminal, it changes the color of all subsequent text until it receives a reset command.

If you forget to include NS_COLOR_RESET at the end of your ns_println call, every single thing your program (and potentially your user’s terminal prompt) prints afterward will be stuck in that format!

Available Macros Reference

Here is a list of the standard text styling macros available in ns_color.h:

Colors:

  • NS_COLOR_RED
  • NS_COLOR_GREEN
  • NS_COLOR_YELLOW
  • NS_COLOR_BLUE
  • NS_COLOR_MAGENTA
  • NS_COLOR_CYAN
  • NS_COLOR_WHITE

Styles:

  • NS_COLOR_BOLD
  • NS_COLOR_UNDERLINE

State Control:

  • NS_COLOR_RESET (Resets all colors and text formatting back to the terminal default)

Strings (ns_string)

In standard C, a string is nothing more than a contiguous array of characters in memory, terminated by a null byte (\0).

While this design is incredibly minimalist, it is also the root cause of countless security vulnerabilities. Because standard C strings do not inherently know their own length or capacity, functions like strcpy and strcat will happily overwrite adjacent memory if the destination buffer is too small, leading to catastrophic buffer overflows.

Furthermore, simply finding the length of a standard C string using strlen() requires an O(N) operation—scanning every single byte until the null terminator is found.

The NextStd Solution

The ns_string module introduces a radically different approach. Instead of raw character pointers, NextStd strings are intelligent structs managed safely by the Rust backend.

By tracking their own length and capacity, NextStd strings make out-of-bounds writes architecturally impossible. If you try to append text that exceeds the current capacity, the Rust backend will automatically and safely reallocate the heap memory for you.

Even better, ns_string implements Small String Optimization (SSO). This means short strings avoid the slow, expensive heap allocation process entirely and are stored directly on the stack, delivering blistering performance that rivals or beats standard C arrays.

What’s in this chapter?

This chapter breaks down how to create, manage, and manipulate memory-safe strings:

  • SSO Architecture: Look under the hood at how NextStd avoids heap allocations for short strings to maximize performance.
  • String Lifecycle: Learn how to initialize, read, and safely free ns_string objects.
  • Concatenation & Manipulation: Discover how to safely append and modify strings without ever worrying about buffer overflows again.

Small String Optimization (SSO)

In standard C, if you want a string that can grow dynamically, you have to use malloc() to allocate memory on the heap.

The problem is that heap allocations are notoriously slow. Requesting memory from the operating system, finding a contiguous block, and managing pointers adds significant overhead. If you are processing thousands of short strings—like parsing names, IP addresses, or configuration keys—this heap allocation bottleneck will severely degrade your application’s performance.

NextStd solves this elegantly using a technique called Small String Optimization (SSO).

How SSO Works

The core idea behind SSO is simple: Why ask the OS for memory if the string is small enough to fit inside the struct itself?

When you initialize an ns_string, the Rust backend checks the length of the text you are trying to store.

  1. The Stack Path (Short Strings): If the string is short (typically under 15-23 characters, depending on the system architecture), NextStd stores the characters directly inside the ns_string struct on the stack. Zero heap allocations are performed.
  2. The Heap Path (Long Strings): If the string exceeds the inline capacity, NextStd automatically falls back to allocating space on the heap and stores a pointer to that data inside the struct.

The Performance Impact

Because most strings in typical applications are short, SSO provides a massive performance boost:

  • Zero Allocation Cost: Creating and destroying short strings is practically instantaneous.
  • Cache Locality: Because the string data lives directly inside the struct on the stack, the CPU can read it without having to jump across RAM to chase a heap pointer. This heavily optimizes CPU cache hits.
  • Safe Fallback: You never have to manually decide between a fixed-size stack array (which risks buffer overflows) and a dynamic heap pointer. The ns_string handles the transition invisibly.

Invisible to the User

The best part about NextStd’s SSO implementation is that you don’t have to think about it.

Whether your string is 5 characters long and living on the stack, or 50,000 characters long and living on the heap, the C API remains exactly the same. You still call ns_string_new to create it, read from its data pointer, and call ns_string_free when you are done.

The Rust backend tracks the memory state internally and guarantees that when ns_string_free is called, it won’t accidentally try to free() a stack-allocated string, completely preventing invalid free crashes.

Creating & Freeing Strings

Unlike standard C strings, which are often just raw pointers to arbitrary blocks of memory, an ns_string is a fully managed data structure.

To guarantee memory safety and prevent undefined behavior, every ns_string must go through a strict, three-step lifecycle: Initialization, Usage, and Destruction.

1. Initialization (ns_string_new)

Before you can use an ns_string, it must be initialized. You do this by passing a pointer to your uninitialized ns_string struct and the initial standard C string (or "" for an empty string) to the ns_string_new function.

This is where the Rust backend takes over. It calculates the length of your input, decides whether to store it on the stack (via Small String Optimization) or allocate memory on the heap, and populates your struct safely.

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_string my_text;

    // 1. Initialize the string
    NS_TRY(err, ns_string_new(&my_text, "Welcome to NextStd!")) {
        ns_println("String initialized successfully.");
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println("Failed to allocate string.");
        return 1;
    }

    // ... usage and destruction ...
    return 0;
}

2. Usage

Once initialized, your ns_string struct contains safely managed properties. Because it utilizes Small String Optimization (SSO), the underlying text data is safely tucked away inside a union.

Fortunately, you don’t have to manually extract it. Because ns_print and ns_println handle ns_string objects natively via C11 _Generic macros, you can simply pass the entire struct directly to print your managed string!

You can also print struct properties like len seamlessly since ns_io natively supports C’s size_t type.

    // 2. Use the string
    ns_print("Message: ");
    ns_println(my_text); // No need to access inner pointers!

    ns_print("Length: ");
    ns_println(my_text.len); // Prints size_t seamlessly

3. Destruction (ns_string_free)

Standard C requires you to manually call free() on any memory you allocate. If you forget, your program leaks memory. If you call it twice, your program crashes (a “double free” vulnerability).

When you are finished with an ns_string, you must pass its pointer to ns_string_free.

    // 3. Destroy the string
    ns_string_free(&my_text);

The Rust backend handles the deallocation process intelligently. If the string was utilizing Small String Optimization (stored entirely on the stack), ns_string_free does practically nothing, safely avoiding an invalid heap operation. If it was allocated on the heap, Rust cleanly returns the memory to the system and nullifies the pointers to prevent accidental use-after-free vulnerabilities.


The Complete Example

Putting it all together, here is the complete, memory-safe lifecycle of an ns_string:

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_string greeting;

    // 1. Initialization
    NS_TRY(err, ns_string_new(&greeting, "Hello, memory safety!")) {

        // 2. Usage
        ns_print("The string is: ");
        ns_println(greeting); 

        ns_print("Length: ");
        ns_println(greeting.len);

    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_print("Error: ");
        ns_println(ns_error_message(err));
        return 1;
    }

    // 3. Destruction (Always clean up!)
    ns_string_free(&greeting);

    return 0;
}

String Concatenation (ns_string_concat)

In standard C, appending text to a string using strcat is one of the most dangerous operations you can perform.

If the destination buffer doesn’t have enough pre-allocated space to hold both the original string and the new text, strcat will silently overwrite adjacent memory. This leads to data corruption, mysterious crashes, and severe security vulnerabilities.

NextStd eliminates this entirely with memory-safe concatenation.

Safe Concatenation

With NextStd, you never have to manually calculate buffer sizes. Instead of mutating an existing string, the ns_string_concat function takes two source ns_string objects and safely writes the combined text into a brand new destination ns_string.

Because combining two strings might require allocating memory on the heap (which can technically fail if the system is out of memory), it returns an ns_error_t that you should wrap in an NS_TRY block.

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_string s1, s2, result;

    // 1. Initialize the source strings
    NS_TRY(err, ns_string_new(&s1, "Hello, ")) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    NS_TRY(err, ns_string_new(&s2, "memory safety!")) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    // 2. Safely concatenate them into the 'result' string
    NS_TRY(err, ns_string_concat(&result, s1, s2)) {
        ns_println("Concatenation successful!");
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_print("Failed to concatenate: ");
        ns_println(ns_error_message(err));
        return 1;
    }

    // 3. Print the combined result
    ns_print("Final message: ");
    ns_println(result);

    // 4. Always clean up all strings!
    ns_string_free(&s1);
    ns_string_free(&s2);
    ns_string_free(&result);

    return 0;
}

How It Works (The SSO Transition)

When you call ns_string_concat, the Rust backend performs a series of safety checks to determine exactly how the new result string should be constructed:

  1. Capacity Calculation: It adds the len of s1 and s2 together to find the exact required size.
  2. The SSO Decision: If the combined length is under 24 bytes, the result string will utilize Small String Optimization (SSO) and live entirely on the stack. Zero heap allocations occur!
  3. Dynamic Reallocation: If the combined length is 24 bytes or larger, the Rust backend safely allocates a new buffer on the heap, copies the data from both source strings into it, and flags the result struct as is_heap.

This entire memory management process happens invisibly. From the C side, you just call ns_string_concat, and NextStd guarantees it succeeds safely or returns a catchable error!

Data Structures (ns_data)

In standard C, managing collections of data requires constant vigilance.

C provides primitive arrays, which are incredibly fast but notoriously dangerous. They have a fixed size, they “decay” into raw pointers when passed to functions (losing all information about their length), and they offer zero bounds checking. If you accidentally write to my_array[10] when the array only has 5 elements, the compiler won’t stop you, leading directly to memory corruption or a segmentation fault.

Furthermore, standard C lacks built-in advanced data structures altogether. If you need a dynamically resizing array or a key-value hash map, you either have to write it yourself from scratch or rely on bloated third-party libraries.

NextStd changes this by introducing the ns_data module: a suite of memory-safe, highly performant data structures backed by Rust.

The NextStd Solution

The data structures in ns_data are designed to feel native to C while providing the airtight memory guarantees of Rust:

  • Dynamic Resizing: You never have to manually call realloc() or calculate byte offsets. The Rust backend automatically scales your collections as you add or remove elements.
  • Strict Bounds Checking: If you attempt to access an index or key that doesn’t exist, NextStd catches it at the FFI boundary and safely returns an ns_error_t instead of crashing your program.
  • Clean Cleanup: Just like strings, calling a single _free macro on your data structure cleanly drops all the heap memory it was managing.

What’s in this chapter?

This chapter breaks down how to initialize, manipulate, and free NextStd’s core collections:

Dynamic Arrays (ns_vec)

In standard C, arrays are rigid. Once you declare int numbers[5];, that array can never hold a sixth number. If you need a collection that can grow or shrink at runtime, you are forced to use raw pointers, malloc, and manual realloc calls.

Worse, standard C arrays have zero bounds checking. If you try to read numbers[100], C will silently read garbage memory from that location, potentially crashing your program or exposing a massive security vulnerability.

NextStd fixes this with ns_vec: a dynamically resizing, bounds-checked vector managed safely by the Rust backend.

1. Initialization (ns_vec_new)

Creating a vector is as simple as creating a string. You declare your ns_vec struct and pass a pointer to ns_vec_new to initialize it. The Rust backend sets up the initial heap allocation and capacity tracking invisibly.

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_vec numbers;

    // 1. Initialize the vector
    NS_TRY(err, ns_vec_new(&numbers)) {
        ns_println("Vector created successfully!");
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println("Failed to allocate vector.");
        return 1;
    }

    // ...
    return 0;
}

2. Adding Elements (ns_vec_push)

You don’t need to keep track of memory capacity or calculate byte offsets to add data. Just call ns_vec_push.

If the vector is full, the Rust backend will automatically and safely allocate a larger block of memory on the heap, move the existing data over, and add your new element. Because this reallocation can technically fail if the system runs out of memory, it returns an ns_error_t.

    // 2. Safely push data into the vector
    NS_TRY(err, ns_vec_push(&numbers, 10)) { } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }
    NS_TRY(err, ns_vec_push(&numbers, 20)) { } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }
    NS_TRY(err, ns_vec_push(&numbers, 30)) { } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

3. Safe Access (ns_vec_get)

This is where NextStd truly shines. When you want to retrieve an element, you pass the index and a pointer to the variable where you want the data stored.

The Rust backend performs a strict bounds check. If you ask for index 5 in a vector that only has 3 elements, your program will not crash. Instead, Rust safely blocks the memory access and returns an error!

    int value;

    // 3. Retrieve an element safely
    NS_TRY(err, ns_vec_get(&numbers, 1, &value)) {
        ns_print("Value at index 1 is: ");
        ns_println(value); // Prints 20
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_print("Out of bounds: ");
        ns_println(ns_error_message(err));
    }

4. Destruction (ns_vec_free)

Just like strings, vectors manage dynamic heap memory. When you are done with the vector, you must free it to prevent memory leaks.

    // 4. Destroy the vector and free the heap memory
    ns_vec_free(&numbers);

The Complete Example

Here is a complete, memory-safe workflow utilizing ns_vec. Notice how clean the C code remains without a single malloc or sizeof in sight:

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_vec scores;

    // Initialize
    NS_TRY(err, ns_vec_new(&scores)) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    // Push values
    NS_TRY(err, ns_vec_push(&scores, 100)) {} NS_EXCEPT(err, NS_ERROR_ANY) {}
    NS_TRY(err, ns_vec_push(&scores, 95)) {} NS_EXCEPT(err, NS_ERROR_ANY) {}

    // Print the length
    ns_print("Total scores: ");
    ns_println(scores.len);

    // Safely retrieve a value
    int current_score;
    NS_TRY(err, ns_vec_get(&scores, 0, &current_score)) {
        ns_print("First score: ");
        ns_println(current_score);
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println(ns_error_message(err));
    }

    // Always clean up!
    ns_vec_free(&scores);

    return 0;
}

Key-Value Maps (ns_hashmap)

If you want to associate a string with an integer, or an ID with a user profile, standard C leaves you entirely on your own. There is no built-in dictionary or hash map data structure. Developers are usually forced to build complex, error-prone linked lists or pull in massive third-party dependencies just to map a key to a value.

NextStd solves this with ns_map: a fast, memory-safe key-value store powered by Rust’s highly optimized standard library hash map, brought to C with a clean and type-safe macro API.

1. Initialization (ns_map_new)

Because ns_map can store any data type, you need to tell it the size of the keys and the values it will be holding when you create it. You do this by passing sizeof(your_key_type) and sizeof(your_value_type) into the ns_map_new initialization function.

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_map student_grades;

    // 1. Initialize a map with `int` keys (Student ID) and `float` values (Grade)
    NS_TRY(err, ns_map_new(&student_grades, sizeof(int), sizeof(float))) {
        ns_println("Map initialized successfully!");
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println("Failed to allocate map.");
        return 1;
    }

    // ...
    return 0;
}

2. Inserting Data (ns_map_put)

In standard C, passing arbitrary generic types into a function is a nightmare of void* casting.

NextStd abstracts all of this away with the ns_map_put macro. You simply provide the map pointer, the type of the key, the key itself, the type of the value, and the value itself. The macro safely handles the memory address referencing behind the scenes.

    // 2. Insert key-value pairs
    // Usage: ns_map_put(&map, key_type, key_data, val_type, val_data)

    NS_TRY(err, ns_map_put(&student_grades, int, 101, float, 95.5f)) {} 
    NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    NS_TRY(err, ns_map_put(&student_grades, int, 102, float, 88.0f)) {} 
    NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

3. Retrieving Data (ns_map_at)

To retrieve a value, you use the ns_map_at macro. You provide the key you are looking for, and a pointer to a variable where NextStd should write the resulting value.

Because hash map lookups can fail (e.g., if you ask for a key that does not exist), ns_map_at returns an error code. This forces you to handle missing keys gracefully instead of accidentally reading uninitialized memory.

    float grade;

    // 3. Retrieve a value by its key
    // Usage: ns_map_at(&map, key_type, key_data, &output_variable)

    NS_TRY(err, ns_map_at(&student_grades, int, 101, &grade)) {
        ns_print("Student 101 Grade: ");
        ns_println_float(grade); // Prints 95.5
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println("Student not found in the map.");
    }

4. Destruction (ns_map_free)

Like all dynamic NextStd structures, ns_map allocates memory on the heap. When you are finished with the map, you must free it to return that memory to the operating system.

    // 4. Clean up the map
    ns_map_free(&student_grades);

The Complete Example

Here is a complete, memory-safe workflow demonstrating how to map integer IDs to float values using ns_map.

#include <nextstd/ns.h>
#include <nextstd/ns_hashmap.h> // Ensure you include the correct header

int main() {
    ns_error_t err;
    ns_map prices;

    // Initialize: int keys (Item ID), float values (Price)
    NS_TRY(err, ns_map_new(&prices, sizeof(int), sizeof(float))) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    // Insert data
    NS_TRY(err, ns_map_put(&prices, int, 1, float, 19.99f)) {} NS_EXCEPT(err, NS_ERROR_ANY) {}
    NS_TRY(err, ns_map_put(&prices, int, 2, float, 5.49f)) {} NS_EXCEPT(err, NS_ERROR_ANY) {}

    // Print the size of the map
    ns_print("Total items in map: ");
    ns_println(prices.len);

    // Retrieve data safely
    float result;
    NS_TRY(err, ns_map_at(&prices, int, 2, &result)) {
        ns_print("Price for Item 2: $");
        ns_println_float(result);
    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_println("Item not found!");
    }

    // Always clean up!
    ns_map_free(&prices);

    return 0;
}

Command Execution (ns_cmd)

Executing shell commands in standard C usually involves reaching for the system() function or wrestling with popen().

Both approaches are fraught with legacy issues. system() offers absolutely no way to capture the output of the command (it just prints directly to the terminal), and popen() requires manually managing streams, allocating buffers, and parsing raw bytes—often leading to memory leaks or buffer overflows.

The ns_cmd module completely replaces these legacy functions with a modern, memory-safe, and highly ergonomic wrapper around Rust’s std::process::Command.

The NextStd Solution

With ns_cmd, executing a command and capturing its exact output is safe and synchronous.

  • Complete Stream Capture: Both stdout and stderr are automatically captured and placed into NextStd’s blazingly fast Small String Optimized (ns_string) structs.
  • Exit Codes: The exact exit code of the shell process is captured and returned.
  • Smart Routing: Pass either a standard C string literal ("uname -a") or a dynamically built ns_string. The macros figure it out automatically.
  • Zero Memory Leaks: Thanks to the ns_autocmd macro, process output structs automatically free their own internal string buffers the moment they go out of scope.

Quick Glance

Here is how simple it is to execute a command, read its output, and clean up the memory—all in four lines of C:

#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
    // 1. Initialize the output struct with the auto-cleanup macro
    ns_autocmd ns_cmd_output out = {0}; 

    // 2. Run the command safely
    ns_cmd_run("uname -a", &out);

    // 3. Use the captured data!
    ns_println("Status: {}", out.exit_code);
    ns_println("Output: {}", out.stdout_data);

    // 4. No free() required. Memory cleans itself up here!
    return 0;
}

Deep Dive

Explore the specific features of the ns_cmd module:

Running Shell Commands

The core of the ns_cmd module is the ns_cmd_run macro. Unlike standard C functions that force you to strictly cast your types or use entirely different function names for different inputs (like print_int vs print_string), NextStd uses modern C11 features to do the heavy lifting for you.

The _Generic Routing Magic

In standard C, if an execution function expected a dynamic string struct, you would be forced to convert your simple string literals into structs every single time you wanted to run a basic command.

NextStd solves this by turning ns_cmd_run into a _Generic macro wrapper.

When you call ns_cmd_run, the C compiler inspects the type of the argument you passed and automatically routes it to the correct Rust FFI backend function:

  • If you pass a "string literal", it routes to ns_cmd_run_cstr.
  • If you pass an ns_string struct, it routes to ns_cmd_run_ns.

1. Passing Standard C Strings

For 90% of your use cases, you will likely just pass a hardcoded string literal. The macro accepts it natively:

#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
    ns_autocmd ns_cmd_output out = {0}; 

    // The compiler sees a const char* and routes it automatically!
    ns_cmd_run("ls -la /var/log", &out);

    return 0;
}

2. Passing Dynamic ns_string Structs

If you are building a command dynamically based on user input or file parsing, you can pass your NextStd ns_string directly into the exact same macro. No unwrapping or .ptr access required:

#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>
#include <nextstd/ns_string.h>

int main(void) {
    ns_autocmd ns_cmd_output out = {0}; 
    ns_string my_dynamic_cmd;

    // Safely build the command
    ns_string_new(&my_dynamic_cmd, "echo 'NextStd is awesome!'");

    // Pass the struct directly!
    ns_cmd_run(my_dynamic_cmd, &out);

    // Clean up the input string
    ns_string_free(&my_dynamic_cmd);

    return 0;
}

Under the Hood: sh -c

When the Rust backend receives your command (regardless of how it was routed), it securely spawns a new process using sh -c "<your_command>".

This mimics the exact behavior of C’s system(), meaning you have full access to standard shell features like piping (|), redirecting (>), and environment variable expansion ($USER), while still running entirely within Rust’s memory-safe execution sandbox.

Capturing Stdout & Stderr

The fatal flaw of standard C’s system() function is that it blindly dumps the command’s output directly to the user’s terminal. If your C program actually needs to read that output to parse a version number, check a status, or log an error, system() is completely useless.

You are usually forced to use popen(), which requires manually allocating buffers, reading chunks in a while loop, and praying you don’t trigger a buffer overflow.

NextStd completely eliminates this friction.

The ns_cmd_output Struct

When you run a command using ns_cmd_run, the entire lifecycle of the process is captured and packed into a single, clean C struct:

typedef struct ns_cmd_output {
    ns_string stdout_data;
    ns_string stderr_data;
    int exit_code;
} ns_cmd_output;

1. The Output Streams (stdout_data & stderr_data)

Instead of raw char* arrays that you have to manually resize, NextStd uses the underlying Rust std::process::Command engine to capture the raw bytes of the streams and convert them directly into memory-safe ns_string structs.

The SSO Advantage: Because these streams are backed by NextStd’s Small String Optimization (SSO), if your command outputs less than 24 bytes (like simply printing "OK" or an exit status string), zero heap allocation occurs. The output is stored directly inside the struct on the stack, making it incredibly fast.

2. The Exit Code

NextStd captures the exact integer status code returned by the shell. You no longer have to mess with C’s cryptic WEXITSTATUS macros just to find out if your command succeeded. If the process exits cleanly, exit_code will be 0.

Example: Parsing Success vs. Failure

Because NextStd strictly separates standard output from standard error, your C program can easily build branching logic based on whether a command failed.

#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
    ns_autocmd ns_cmd_output out = {0};

    // Attempting to read a file that doesn't exist
    ns_cmd_run("cat /path/to/missing_file.txt", &out);

    if (out.exit_code != 0) {
        ns_println("Command failed with code: {}", out.exit_code);
        // We can safely read the exact error message the shell generated
        ns_println("Error Reason: {}", out.stderr_data);
    } else {
        ns_println("File Contents: {}", out.stdout_data);
    }

    return 0;
}

By default, these strings are dynamically allocated (if over 23 bytes) and require cleanup. However, thanks to a powerful compiler extension, you rarely ever need to free them manually.

Auto-Cleanup (RAII in C)

One of the biggest pain points in standard C is manual memory management. If a library dynamically allocates memory to capture a string or a file, the developer is strictly responsible for calling free() when they are done.

If you forget, your program leaks memory. If you call it twice, your program crashes.

Languages like C++ and Rust solved this decades ago using RAII (Resource Acquisition Is Initialization) and destructors. When a variable goes out of scope (reaches the closing brace } of its block), the compiler automatically inserts the cleanup code.

With NextStd, you get that exact same magic in C.

The ns_autocmd Macro

NextStd leverages a powerful, widely-supported GCC/Clang compiler extension called __attribute__((cleanup)). We wrap this extension in a simple, ergonomic macro called ns_autocmd.

When you prefix your ns_cmd_output struct with ns_autocmd, you are attaching a hidden destructor to the variable. The moment that variable falls out of scope, the compiler automatically intercepts it and safely frees any internal ns_string heap allocations.

#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

void run_status_check() {
    // 1. Declare the struct with the auto-cleanup macro
    ns_autocmd ns_cmd_output out = {0}; 

    // 2. Run the command
    ns_cmd_run("uptime", &out);

    // 3. Print the result
    ns_println("System Uptime: {}", out.stdout_data);

    // 4. End of scope. The compiler automatically calls 
    // ns_cmd_output_free(&out) right here! No memory leaks!
}

⚠️ The Golden Rule: Zero-Initialization

There is one critical rule when using ns_autocmd: You must always zero-initialize your struct (= {0};).

// ❌ DANGEROUS: Contains random stack garbage memory
ns_autocmd ns_cmd_output bad_out; 

// ✅ SAFE: Memory is explicitly zeroed out
ns_autocmd ns_cmd_output good_out = {0}; 

Why is this required? In C, uninitialized stack variables contain random “garbage” data left over in RAM.

If ns_cmd_run were to fail before it could successfully populate your struct (for example, if the system couldn’t spawn a shell), the destructor would eventually run and try to free() that random garbage memory. This results in a guaranteed segmentation fault.

By zero-initializing the struct (= {0};), you guarantee that if the command fails early, the destructor simply sees NULL pointers and lengths of 0, and safely does nothing.

Manual Cleanup

If you are compiling on a highly restrictive embedded compiler that does not support GCC/Clang extensions, or if you simply prefer manual memory management, you can still declare the struct normally and use the manual free function:

ns_cmd_output out = {0};
ns_cmd_run("ls", &out);
ns_println("Output: {}", out.stdout_data);

// Manually free the internal strings
ns_cmd_output_free(&out);

Practical Examples

In the previous chapters, we explored the individual building blocks of NextStd: handling errors gracefully, printing to the terminal, managing dynamic memory, and safely storing data.

However, the true power of this library becomes apparent when you combine these modules. When ns_io, ns_error, and ns_string work together, they transform standard C from a language of constant manual memory vigilance into a modern, safe, and highly productive tool.

What’s in this chapter?

This section provides complete, copy-pasteable programs that demonstrate how to solve common C programming challenges without writing a single malloc, free, or dangerous buffer manipulation.

  • Safe User Input: Learn how to capture dynamic terminal input of any length safely, completely eliminating the buffer overflow vulnerabilities of scanf and gets.
  • Building a CLI Menu: Combine colored printing, safe integer reading, and error handling to create a robust interactive terminal application.
  • String Manipulation: See how to safely build and combine dynamic strings without worrying about capacity calculations or strcat overwriting adjacent memory.

Whether you are building a small command-line utility or a larger system application, these examples serve as a blueprint for writing memory-safe C code with NextStd.

Safe User Input

Taking text input from a user in standard C is notoriously dangerous.

Historically, functions like gets() were so prone to catastrophic buffer overflows that they were completely removed from the C language standard. Even the modern alternative, scanf("%s", buffer), is risky. If you allocate a char buffer[50]; and the user types 100 characters, the program will silently overwrite adjacent memory, leading to crashes or severe security vulnerabilities.

To fix this in standard C, you have to write complex loops that read one character at a time and manually reallocate heap memory on the fly. NextStd handles all of this for you automatically.

The NextStd Solution: Dynamic Reading

With NextStd, the philosophy is simple: You should never have to guess how much text a user is going to type.

By passing an ns_string directly into the ns_read macro, the Rust backend takes over. It safely reads the entire line from standard input and automatically calculates the exact memory required.

  • If the user types a short name (under 24 bytes), NextStd uses Small String Optimization (SSO) and stores it entirely on the stack.
  • If the user pastes a 10,000-word essay into the terminal, NextStd seamlessly allocates the exact amount of heap space needed.

No buffers to manage, no arbitrary limits, and absolutely no memory corruption.

Step-by-Step Walkthrough

Capturing user input dynamically requires just a few simple steps:

  1. Declare your string: You only need an uninitialized ns_string struct.
  2. Prompt the user: Ask your question using ns_print.
  3. Capture the input: Pass a pointer to your string into ns_read, wrapping it in an NS_TRY block to safely catch any unexpected I/O failures.
  4. Use the data: Print or manipulate the string safely.
  5. Clean up: Always pass the string to ns_string_free when you are done to release any heap memory it may have allocated.

The Complete Example

Here is a fully functional, memory-safe program that captures user input of any length dynamically:

#include <nextstd/ns.h>

int main() {
    ns_error_t err;
    ns_string user_input;

    // 1. Ask the user a question
    ns_print("Please enter your name (or paste a whole paragraph!): ");

    // 2. Read the input dynamically
    NS_TRY(err, ns_read(&user_input)) {

        // 3. Use the safely captured string
        ns_print("Hello, ");
        ns_println(user_input);

        ns_print("Your input was exactly ");
        ns_print(user_input.len);
        ns_println(" characters long.");

    } NS_EXCEPT(err, NS_ERROR_ANY) {
        ns_print("Failed to read input: ");
        ns_println(ns_error_message(err));
        return 1;
    }

    // 4. Always clean up! 
    // (Safely frees heap memory, or does nothing if the string used SSO)
    ns_string_free(&user_input);

    return 0;
}

Because ns_read is heavily integrated with the NextStd _Generic macros, you never have to specify format strings like %s or %d. The compiler inherently knows you are reading into an ns_string and routes it to the exact memory-safe Rust function required!

Building a CLI Menu

One of the most common tasks in systems programming is building an interactive Command Line Interface (CLI) menu. However, doing this in standard C often introduces a severe bug: the infinite input loop.

If you build a menu using a standard while loop and ask for the user’s choice using scanf("%d", &choice), your program expects an integer. If the user accidentally types a letter (like “A”), scanf fails to read it, but it leaves the letter in the input buffer. The loop repeats, scanf sees the same letter, fails again, and your terminal is instantly flooded with an infinite loop of repeated prompts.

NextStd completely eliminates this class of bugs.

The NextStd Approach

By combining ns_read, our ns_color macros, and NS_TRY/NS_EXCEPT blocks, you can build a menu that is completely bulletproof.

When you use ns_read(&op), the Rust backend safely reads the input line. If the user types “A” instead of a number, it catches the parsing error, flushes the bad input, and cleanly throws an NS_ERROR_INVALID_INPUT error. Your program can catch it, print a polite warning, and safely wait for the next input.

The Complete Example

Here is a fully functional “System Control” menu. Try running this and intentionally typing words instead of numbers—notice how it handles the errors gracefully without ever crashing or looping infinitely!

#include <nextstd/ns.h>
#include <nextstd/ns_color.h>
#include <stdbool.h>

int main(void)
{
  int op = 0;     // Option
  ns_error_t err;
  bool continue_loop = true;

  while (continue_loop) {
    ns_println("Choose any of the below options: ");
    ns_println("1. First Option\n2. Second Option \n3. Third Option \n4. Exit");
    ns_print("Enter your option: ");
    NS_TRY(err, ns_read(&op)) {
      if (op == 1) {
        ns_println(NS_COLOR_GREEN "You chose option 1" NS_COLOR_RESET);
      }
      else if (op == 2) {
        ns_println(NS_COLOR_BLUE "You chose option 2" NS_COLOR_RESET);
      } else if (op == 3) {
        ns_println(NS_COLOR_MAGENTA "You chose option 3" NS_COLOR_RESET);
      } else {
        ns_println(NS_COLOR_YELLOW "Exiting...." NS_COLOR_RESET);
        continue_loop = false;
      }
    }
    NS_EXCEPT(err, NS_ERROR_INVALID_INPUT) {
      ns_println(NS_COLOR_RED "Wrong input" NS_COLOR_RESET);
      ns_println(ns_error_message(err));
    }
  }
}

Why this works so well

  • State Management: The continue_loop boolean gives us a clean way to break out of the infinite while loop when the user selects the Exit option.
  • Aesthetic Feedback: The ns_color macros provide instant visual feedback. Successes are green, information is blue/magenta, warnings are yellow, and errors are red.
  • Absolute Stability: The NS_TRY block isolates the unpredictable nature of human input, guaranteeing the program state remains valid.

Safe String Manipulation

In standard C, combining strings is one of the most dangerous and tedious operations you can perform.

Because standard C strings are just primitive arrays of characters ending in a null byte (\0), they have no concept of their own capacity. If you want to combine two strings using strcat(), you must first manually calculate the length of both strings, allocate a completely new buffer large enough to hold them both, and carefully check for off-by-one errors. Forgetting just one of these steps results in a buffer overflow that will silently overwrite adjacent memory.

NextStd eliminates this entirely.

The NextStd Solution: Safe Concatenation

Because every ns_string is a managed struct backed by Rust, it tracks its own length, capacity, and allocation state (SSO vs. Heap).

When you want to combine two strings, you simply pass them to ns_string_concat wrapped in an NS_TRY block. The Rust backend instantly calculates the required capacity for the combined string, safely allocates a new block of memory (or uses Small String Optimization if the combined length is short enough), and writes the result into your destination struct—all behind the scenes.

The Complete Example

Here is a practical example demonstrating how to safely combine two ns_string objects. Notice how there are zero malloc(), realloc(), or strlen() calls!

#include <nextstd/ns.h>
#include <nextstd/ns_string.h>

int main(void) 
{
    ns_error_t err;
    ns_string greeting;
    ns_string name;
    ns_string final_message;

    // 1. Initialize our starting strings
    NS_TRY(err, ns_string_new(&greeting, "Hello, ")) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    NS_TRY(err, ns_string_new(&name, "World!")) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    // 2. Safely concatenate them into final_message
    NS_TRY(err, ns_string_concat(&final_message, greeting, name)) {
    } NS_EXCEPT(err, NS_ERROR_ANY) { return 1; }

    // 3. Print the safely resized, concatenated string!
    ns_println(final_message);

    ns_print("Final String Length: ");
    ns_println(final_message.len);

    // 4. Always clean up all initialized strings
    ns_string_free(&greeting);
    ns_string_free(&name);
    ns_string_free(&final_message);

    return 0;
}

Why this is completely safe

  • No Overflows: The backend automatically calculates the exact memory required for final_message. If the combined string is over 24 bytes, it smoothly and invisibly transitions to a heap allocation.
  • Error Handling: If the system is completely out of memory and cannot allocate the combined buffer, the NS_TRY block will gracefully catch the NS_ERROR_ALLOCATION_FAILED error instead of crashing the program with a segmentation fault.
  • Clean Deallocation: Calling ns_string_free at the end drops the memory safely for all three structs.

Process Execution & Auto-Free

Contributing to NextStd

Welcome, and thank you for your interest in contributing to NextStd!

Standard C is foundational to modern computing, but its lack of memory safety continues to be one of the largest sources of vulnerabilities in software today. By contributing to this project, you are helping bridge the gap between C’s unmatched portability and Rust’s airtight memory guarantees.

Because NextStd is a hybrid project—combining C11 _Generic macros on the front-end with an optimized Rust Foreign Function Interface (FFI) on the back-end—working on it is a unique and highly rewarding engineering challenge.

What’s in this section?

Whether you want to fix a bug, optimize an existing FFI boundary, or build an entirely new memory-safe data structure, these guides will get you up to speed on how the codebase operates:

  • Architecture Overview: Understand the data flow between the C headers, the _Generic router macros, and the Rust unsafe extern "C" backend.
  • Adding New Modules: A step-by-step guide on how to conceptualize, write, and safely expose a new Rust-backed feature to the C front-end.

Prerequisites for Developing

To compile the codebase and run the test suites locally, you will need:

  • A standard C compiler (gcc or clang)
  • The Rust toolchain (cargo and rustc)
  • make (for executing the build and linking scripts)

We are thrilled to have you on board. Let’s make C safer, together!

Architecture Overview

NextStd is not a traditional C library. It is a hybrid architecture designed to seamlessly bridge the gap between two radically different languages.

To a developer using the library, NextStd looks and feels exactly like modern C. They include a header, call a macro, and their code compiles cleanly. Under the hood, however, there is almost no C execution happening. The C code acts entirely as a lightweight routing layer that safely passes data across a Foreign Function Interface (FFI) boundary into a highly optimized, memory-safe Rust engine.

The Three Pillars of NextStd

Understanding how data flows through the library requires looking at its three distinct layers.

1. The C Front-End (include/nextstd/)

This is the only part of the library the end-user ever sees. It consists entirely of header files containing:

  • Memory-Layout Structs: C structs (like ns_string or ns_vec) that define the exact byte layout of the data so the C compiler knows how much memory to reserve on the stack.
  • _Generic Macros: The secret weapon of NextStd. Because C does not support function overloading, we use C11 _Generic macros to inspect the type of a variable at compile time and automatically route it to the correct underlying function (e.g., routing an int to ns_print_int and an ns_string to ns_print_ns_string).
  • Error Handling Macros: The NS_TRY and NS_EXCEPT macros that evaluate the ns_error_t enums returned by the backend.

2. The FFI Boundary

The Foreign Function Interface (FFI) is the bridge between C and Rust. Because Rust and C manage memory differently, crossing this bridge requires strict rules:

  • No C++ Magic: Everything must use standard C ABI (Application Binary Interface).
  • Pointer Passing: When C needs Rust to modify a struct (like allocating heap memory for a new string), C passes a raw pointer (ns_string*) across the boundary.
  • Universal Error Currency: Rust functions never crash or “panic” across the boundary. Every fallible operation returns an ns_error_t enum back to C.

3. The Rust Back-End (crates/)

This is where the actual logic lives. The Rust backend exposes functions marked with #[unsafe(no_mangle)] pub extern "C".

  • The “Unsafe” Gateway: When Rust receives a raw pointer from C, it must enter an unsafe {} block to dereference it.
  • The Safe Execution: Once the raw C data is converted into safe Rust types (like turning a *const c_char into a CStr), the rest of the execution happens in 100% safe Rust. Rust handles the dynamic memory allocation, the string formatting, and the bounds checking.
  • The Clean Return: Rust updates the memory at the C pointer address, converts any internal Rust errors into an ns_error_t, and cleanly returns control to C.

The Lifecycle of a Function Call

To picture how this all comes together, let’s trace exactly what happens when a user calls ns_read(&my_text):

  1. Compile Time (C Front-End): The C preprocessor evaluates ns_read(&my_text). It sees that &my_text is an ns_string* type. Using the _Generic macro, it replaces the code with a call to ns_read_ns_string(&my_text).
  2. Execution (Crossing the FFI): The compiled C program jumps to the memory address of ns_read_ns_string.
  3. Rust Takes Over (Back-End): The Rust function pub extern "C" fn ns_read_ns_string(ptr: *mut NsString) wakes up. It safely reads dynamic input from the terminal using Rust’s std::io.
  4. Memory Allocation: Rust calculates the exact size of the input, allocates the necessary heap memory, and carefully writes the pointer addresses and capacity sizes directly into the ptr struct.
  5. Return: Rust returns NsError::Success (which maps perfectly to NS_ERROR_SUCCESS in C). Control is handed back to the C program.

By keeping the C layer incredibly thin and offloading all the complex logic to Rust, NextStd guarantees memory safety without sacrificing the ease of use of a C library.

Adding New Modules

Because NextStd relies on a hybrid architecture, adding a new feature isn’t as simple as just writing a new C function. You are essentially building a bridge: constructing the heavy machinery in Rust, and then creating a clean, easy-to-use control panel for it in C.

This guide will walk you through the standard workflow for contributing a new module to the library.

Step 1: Build the Rust Backend

Every new feature starts in the crates/ directory. You will either add a new file to an existing crate (like ns_io or ns_string) or create an entirely new crate for a major feature.

When writing your Rust logic, keep the FFI (Foreign Function Interface) boundary in mind:

  • Use #[unsafe(no_mangle)]: This prevents the Rust compiler from renaming your function, ensuring the C linker can find it by its exact name.
  • Use extern "C": This forces Rust to use the C Application Binary Interface (ABI).
  • Handle Pointers Safely: C will pass raw pointers (*mut T or *const T). You must check them for null before dereferencing them inside an unsafe {} block.
#![allow(unused)]
fn main() {
// Example: crates/ns_math/src/lib.rs

use ns_error::NsError;
use std::os::raw::c_int;

#[unsafe(no_mangle)]
pub unsafe extern "C" fn ns_math_add(a: c_int, b: c_int, out: *mut c_int) -> NsError {
    if out.is_null() {
        return NsError::Any; // Never trust the C pointer!
    }

    // Perform the safe Rust logic
    let result = a.saturating_add(b);

    // Write back to the C pointer safely
    unsafe { *out = result; }

    NsError::Success
}
}

Step 2: Define the C Interface

Once the Rust engine is built, you need to expose it to the C developers. This happens in the include/nextstd/ directory.

Create a new header file (e.g., ns_math.h). Inside, you will:

  1. Declare the external function exactly as it is named in Rust.
  2. Create a user-friendly macro to hide the pointer logic and error handling if necessary.
// Example: include/nextstd/ns_math.h

#ifndef NS_MATH_H
#define NS_MATH_H

#include "ns_error.h"

#ifdef __cplusplus
extern "C" {
#endif

// 1. Expose the Rust function
ns_error_t ns_math_add(int a, int b, int* out);

// 2. Create a clean macro for the user
#define ns_add(a, b, out_ptr) ns_math_add((a), (b), (out_ptr))

#ifdef __cplusplus
}
#endif

#endif // NS_MATH_H

Step 3: Integrate _Generic (If Applicable)

If your new module handles multiple data types (like printing or reading), you should use C11 _Generic macros to route the data automatically.

For example, if you added ns_math_add_float in Rust, your C macro would look like this:

#define ns_add(a, b, out_ptr) _Generic((out_ptr), \
    int*: ns_math_add, \
    float*: ns_math_add_float \
)(a, b, out_ptr)

Step 4: Hook up the Build System

Finally, ensure your new Rust crate is included in the Cargo workspace (if it’s a new crate) and that the compiled static or dynamic library is properly linked in the project’s Makefile.

Run make rust to compile your new backend, write a quick test script in main.c, and verify that your C macros successfully trigger your safe Rust code!