Skip to content

Ferroni

Oniguruma, continued in Rust. A regex engine in memory-safe Rust, with the vscode-oniguruma scanner built in for TextMate grammars. Verified against the upstream tests and measured against the C original.

cargo add ferroniReleases on crates.io

Oniguruma ended. The engine goes on.

Oniguruma’s C project closed on April 24, 2025, after more than twenty years as the regex engine that TextMate grammars are written for. Ferroni carries it forward.

Same engine, verified

A line-by-line port that keeps Oniguruma's module structure and optimization pipeline. Verified against the upstream UTF-8 tests, with differential checks against C.

Memory-safe, no C toolchain

cargo add ferroni and build: no bindgen, no C compiler, no node-gyp. The limited unsafe code follows two documented patterns in ADR-002.

Measured on real code

From individual regex searches to complete TextMate grammars, the benchmark reports record input, timings, and reproduction steps alongside the tradeoffs.

The scanner, built in

vscode-textmate and Shiki tokenize through vscode-oniguruma's scanner. Ferroni ships a scanner of the same shape, UTF-16 offsets included, next to the regex engine.

Where Ferroni sits

Ferroni supplies the regex engine for Ferriki, whose Shiki-compatible highlighter uses TextMate grammars to tokenize code. Ferromark combines that highlighting with Markdown rendering. Each tool also works on its own.

InputMarkdown with code01ferroniRegex engine02ferriki (GitHub repository)Syntax highlighting03ferromarkMarkdown to HTMLOutputHighlighted HTML

A regular expression, run

A lookbehind selects the date; named groups return its parts. This is the committed output of the Rust example shown here.

Run the example: cargo run --example website_sample

website_sample.rs
use ferroni::prelude::*;

fn main() -> Result<(), RegexError> {
    let regex = Regex::new(r"(?<=Date: )(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})")?;
    let text = "Date: 2026-09-26";
    let captures = regex.captures(text).expect("the sample date matches");

    println!("input: {text}");
    for name in ["year", "month", "day"] {
        let value = captures.name(name).expect("the named group exists");
        println!("{name}: {}", value.as_str());
    }
    Ok(())
}
website_sample.rsRust source
website_sample.rs
use ferroni::prelude::*;

fn main() -> Result<(), RegexError> {
    let regex = Regex::new(r"(?<=Date: )(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})")?;
    let text = "Date: 2026-09-26";
    let captures = regex.captures(text).expect("the sample date matches");

    println!("input: {text}");
    for name in ["year", "month", "day"] {
        let value = captures.name(name).expect("the named group exists");
        println!("{name}: {}", value.as_str());
    }
    Ok(())
}
Ferroni 1.6.0 · stdout
input: Date: 2026-09-26
year: 2026
month: 09
day: 26
Read the output as text
input: Date: 2026-09-26
year: 2026
month: 09
day: 26

Measured against C Oniguruma

Each factor is Oniguruma’s time divided by Ferroni’s on the same input, higher is faster. The highlighting rows tokenize whole documents line by line, each line handed to the scanner once, the way vscode-textmate and Shiki drive it.

Reference measurements from 2026-09-23 with battle_bench at commit 2f109a75, on a MacBookPro18,1 (Apple M1 Pro, 32 GB), macOS 27.0. These predate the latest optimizations. Full tables and reproduction: Benchmark Results. More recent measurements: Simple-pattern profiling.

TypeScript Document
2.5×
279 patterns, 28 lines, line by line~1.26 ms vs ~3.13 ms
CSS Document
32.6×
117 patterns, 19 lines, line by line~93 µs vs ~3.04 ms
Rust Document
9.8×
81 patterns, 31 lines, line by line~108 µs vs ~1.06 ms
Rejection Speed
6.2×
No match in 50 KB buffer~1.5 µs vs ~9.3 µs
RegSet Multi-Pattern
3.6×
5 patterns, simultaneous search~104 ns vs ~370 ns
Lookaround Combined
3.1×
Feature most Rust engines skip~79 ns vs ~247 ns

Regex engine and scanner, one crate

Match with the idiomatic Regex API, or tokenize with the multi-pattern Scanner that TextMate grammars need. Both come with cargo add ferroni.

main.rs
use ferroni::prelude::*;

fn main() -> Result<(), RegexError> {
    let re = Regex::new(r"(?<year>\d{4})-(?<month>\d{2})")?;

    let caps = re.captures("Date: 2026-02-12").unwrap();
    assert_eq!(caps.name("year").unwrap().as_str(), "2026");
    Ok(())
}
highlight.rs
use ferroni::scanner::{Scanner, ScannerFindOptions};

let mut scanner = Scanner::new(&[
    r"\b(function|const|let|var)\b",
    r#""[^"]*""#,
    r"//.*$",
]).unwrap();

let m = scanner
    .find_next_match(r#"const x = "hello""#, 0, ScannerFindOptions::NONE)
    .unwrap();
assert_eq!(m.index, 0); // "const" matched first

What it covers

Ferroni ports ASCII and UTF-8, two of Oniguruma’s 29 encodings, and leaves out the POSIX and GNU APIs (ADR-003, ADR-012). For the projects built on Oniguruma’s syntax, that means:

  • TextMate grammars, VS Code, ShikiCovered

    vscode-oniguruma compiles every pattern as UTF-8. Ferroni's scanner keeps its API shape and its UTF-16 offsets.

  • jqCovered

    jq matches on UTF-8 text, which Ferroni handles in full.

  • PHP mbregexUTF-8 only

    mb_ereg in Shift_JIS, EUC-JP or another non-UTF-8 encoding is not covered.

  • RubyNot a target

    Ruby runs Onigmo, a fork of Oniguruma with its own history and encodings.

Start building with Ferroni

Oniguruma’s engine and the vscode-oniguruma scanner in one pure-Rust crate. Verified against the upstream tests, measured against the C original.