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.
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
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
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(())
}
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.
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(())
}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 firstWhat 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.