This document provides detailed documentation for the Zig G-code Parser, a high-performance, robust, and configurable library for parsing G-code files and streams.
The parser is designed with the following principles in mind:
- Performance: It uses a zero-allocation iterative parsing model (
next()) for memory-constrained environments and a fast state-machine-based line parser. - Flexibility: It can ingest G-code from files, in-memory buffers (slices), or any custom
std.io.AnyReaderstream. - Safety: It enforces configurable resource limits to protect against malformed or malicious input.
- Configurability: Its behavior can be fine-tuned through a comprehensive set of options to support various G-code dialects, including custom address letters, checksum validation, and more.
The core of the library is a generic Parser(FloatType) struct, which can be instantiated with f32 or f64 depending on the required floating-point precision.
This example demonstrates the simplest way to parse a G-code file and print its contents.
const std = @import("std");
const GCodeParser = @import("gcode_parser.zig").Parser(f32);
pub fn main() !void {
// Initialize an allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
// Create a dummy G-code file for the example
const file_path = "example.gcode";
try std.fs.cwd().writeFile(file_path,
\\(This is a sample G-code file)
\\G90 G21 ; Use absolute coordinates in mm
\\G01 X10.5 Y20.0 Z-1.0 F1200
\\M03 S1500 ; Start spindle
);
defer std.fs.cwd().deleteFile(file_path) catch {};
// 1. Create a parser instance from the file
var parser = try GCodeParser.fromFile(allocator, file_path, null);
defer parser.deinit(); // Ensures file handle and memory are released
// 2. Iterate through each block (line) of G-code
while (try parser.next()) |block| {
std.debug.print("Parsed Line {d}:", .{block.line_number});
// 3. Process the words in the block
for (block.words) |word| {
switch (word.value) {
.float => |f| std.debug.print(" {c}{d}", .{word.letter, f}),
.string => |s| std.debug.print(" {c}\"{s}\"", .{word.letter, s}),
}
}
std.debug.print("\n", .{});
}
}Parsed Line 2: G90.0 G21.0
Parsed Line 3: G1.0 X10.5 Y20.0 Z-1.0 F1200.0
Parsed Line 4: M3.0 S1500.0-
Instantiation: Create a
Parserinstance using one of the three factory methods:fromFile(allocator, path, ?options)fromSlice(allocator, buffer, ?options)fromReader(allocator, reader, ?options)
-
Parsing: Process the G-code using one of two methods:
next(): Iterates through the source one block at a time. This is the most memory-efficient method, as it reuses an internal buffer.collect(): Parses the entire source at once and returns aParseResultcontaining all blocks. This is convenient but uses more memory.
-
Deinitialization: Always call
parser.deinit()to release allocated memory. If created withfromFile, this also closes the file handle.
The parser tokenizes G-code into a hierarchy of Blocks and Words.
Word: The smallest semantic unit in G-code, consisting of an addressletter(e.g., 'G') and avalue(a float or a string).{ .letter = 'G', .value = .{ .float = 1.0 } }
Block: Represents a single effective line of G-code. It contains a slice ofwordsand theline_numberfrom the source.
A key concept when using the next() method is that the returned Block is ephemeral. Its words slice points to an internal buffer that will be overwritten on the next call to next().
If you need to store a block for later use, you must create a deep copy using block.toOwned(allocator).
// Inside a `while (try parser.next()) |block|` loop:
// This is UNSAFE if you store `block` directly
my_list.append(block); // DANGEROUS: `block.words` will be invalid later
// This is SAFE
const owned_block = try block.toOwned(allocator);
try my_list.append(owned_block);The collect() method returns a ParseResult where all blocks and words are already owned and stable in memory.
A generic struct that provides the core parsing functionality.
Generic Parameters
FloatType: The floating-point type to use for numeric values. Must bef32orf64.
pub fn fromFile(
allocator: std.mem.Allocator,
file_path: []const u8,
options: ?ParserOptions
) !@This()Creates a parser that reads from a file at the given path. The parser takes ownership of the file handle and will automatically close it when deinit() is called.
- Parameters:
allocator: The memory allocator for internal buffersfile_path: The path to the G-code fileoptions: An optionalParserOptionsstruct to configure parsing behavior
- Returns: A new
Parserinstance - Errors:
std.fs.File.OpenError,std.mem.Allocator.Error
pub fn fromSlice(
allocator: std.mem.Allocator,
gcode_slice: []const u8,
options: ?ParserOptions
) !@This()Creates a parser that reads from an in-memory byte slice.
- Parameters:
allocator: The memory allocator for internal buffersgcode_slice: A slice containing the G-code textoptions: An optionalParserOptionsstruct to configure parsing behavior
- Returns: A new
Parserinstance - Errors:
std.mem.Allocator.Error
pub fn fromReader(
allocator: std.mem.Allocator,
reader: std.io.AnyReader,
options: ?ParserOptions
) !@This()Creates a parser from an existing AnyReader stream. The caller is responsible for managing the lifetime of the underlying stream.
- Parameters:
allocator: The memory allocator for internal buffersreader: TheAnyReaderto read G-code fromoptions: An optionalParserOptionsstruct to configure parsing behavior
- Returns: A new
Parserinstance - Errors:
std.mem.Allocator.Error
pub fn deinit(self: *@This()) voidReleases all resources used by the parser, including internal buffers and any owned file handles. Must be called to prevent memory leaks.
pub fn next(self: *@This()) ParseError!?BlockParses and returns the next block of G-code from the source.
- Returns:
?Block: An optionalBlock. The block is ephemeral and its data is only valid until the next call tonext()null: When the end of the stream is reached
- Errors:
ParseErrorif a parsing issue occurs
pub fn collect(self: *@This()) !ParseResultParses the entire remaining G-code stream and returns a ParseResult containing all blocks and words in owned memory.
- Returns: A
ParseResultstruct. The caller is responsible for callingresult.deinit(allocator)to free its memory - Errors:
ParseError,std.mem.Allocator.Error
The ParserOptions struct allows you to customize the parser's behavior. Pass it as the final argument to a factory method.
const options = GCodeParser.ParserOptions{
.validate_checksum = true,
.limits = .{ .max_lines = 500000 },
};
var parser = try GCodeParser.fromFile(allocator, "path.gcode", options);| Field | Type | Default | Description |
|---|---|---|---|
address_config |
AddressConfig |
AddressDialects.FULL |
Defines the set of accepted command letters (e.g., G, M, X, Y, Z) |
limits |
Limits |
Default limits | Sets resource limits to prevent memory exhaustion from malicious input |
strict_comments |
bool |
true |
If true, an unclosed parenthetical comment ( is a ParseError |
skip_empty_lines |
bool |
true |
If true, blank lines are ignored. If false, they are treated as empty blocks |
ignore_unknown_characters |
bool |
true |
If true, silently skips characters that are not part of valid G-code syntax |
support_quoted_strings |
bool |
true |
If true, enables parsing of quoted string values, e.g., P"my_string" |
validate_checksum |
bool |
true |
If true, validates line checksums (e.g., *57). Mismatches cause a ParseError |
validate_line_numbers |
bool |
true |
If true, ensures N words are positive, sequential integers |
| Field | Type | Default | Description |
|---|---|---|---|
max_input_size |
?usize |
100 MB | Maximum total bytes to read from the source |
max_blocks |
?usize |
1,000,000 | Maximum number of blocks to parse |
max_words_per_block |
?usize |
50 | Maximum number of words allowed in a single block |
max_line_length |
?usize |
16 KB | Maximum length of a single line in bytes |
max_lines |
?usize |
2,000,000 | Maximum number of lines to read |
The next() method can return a ParseError union. Your code should handle these potential errors.
while (parser.next()) |block| {
// process block
} catch |err| {
switch (err) {
error.UnclosedComment => std.debug.print("Error: Unclosed comment on line {d}\n", .{parser.line_number}),
error.InvalidNumber => std.debug.print("Error: Malformed number on line {d}\n", .{parser.line_number}),
error.InputTooLarge => std.debug.print("Error: Input file exceeds size limits.\n", .{}),
else => std.debug.print("An unexpected parsing error occurred: {any}\n", .{err}),
}
}EmptyValue: A command letter was not followed by a value (e.g.,G)InvalidNumber: A value was not a valid float (e.g.,X10.5.2)UnclosedComment: A(was not closed by a)UnclosedString: A"was not closed by another"UnexpectedCharacter: An invalid character was found (e.g., a digit before a letter)OutOfMemory: An allocation failedInputTooLarge: The input source exceededlimits.max_input_sizeIoFailure: A low-level error occurred while reading from the sourceTooManyBlocks: Exceededlimits.max_blocksTooManyLines: Exceededlimits.max_linesTooLongLine: Exceededlimits.max_line_lengthBlockTooLarge: Exceededlimits.max_words_per_blockChecksumMismatch: The calculated checksum did not match the provided oneInvalidChecksum: The checksum value was not a valid numberInvalidLineNumber: An N word was not a positive, sequential integer