vectorscan4j is a Java wrapper around the high-performance, multi-regex pattern matching engine Vectorscan. It supports the simultaneous matching of thousands of different string- and/or regex patterns on input data. Vectorscan uses a Finite State Machine-based execution through a hybrid DFA/NFA automaton.
- Features
- Installation
- General Usage Framework
- Code Example
- Database Serialization/Deserialization
- Incorrect Usage Patterns
- Limitations
- Third-Party Licenses
- Contributing
- Wraps Vectorscan's optimized engine to enable fast, SIMD-powered multi-regex pattern matching in Java.
- Supports all common regex features.
- Provides additionally a wide set of different feature flags that can be set for each pattern individually (for example enabling UTF8 support, case-insensitive matching, and more).
- Supports two different modes of execution:
- Block mode: Scan complete input buffers in isolation (the "default" execution mode).
- Streaming mode: Maintains the internal state across multiple scan calls (supports matches spanning chunk boundaries). This is useful in a streaming/networking context, where data is split up in chunks.
- It has a flexible callback-based match processing. The user can provide custom logic that is executed whenever a match is found (see the code example).
- Uses Java's new Foreign Function & Memory API for safe and efficient interoperability between JVM- and native execution.
Find all published versions on Maven Central: https://central.sonatype.com/artifact/com.dynatrace.vectorscan4j/vectorscan4j
<dependency>
<groupId>com.dynatrace.vectorscan4j</groupId>
<artifactId>vectorscan4j</artifactId>
<version>{latest-vectorscan4j-version}</version> <!-- see Maven Central badge above -->
</dependency>implementation "com.dynatrace.vectorscan4j:vectorscan4j:{latest-vectorscan4j-version}" // see Maven Central badge above - Java 25+ (uses the Foreign Function & Memory API)
- Linux (x86-64 or aarch64) — native Vectorscan binaries are bundled in the JAR
The usage of vectorscan4j for regex-pattern-matching is split up into multiple steps:
-
Compiling a "database" from a set of literal/regex patterns: Given a fixed set of literal/regex patterns, you compile a Database object. A Database object holds an encoding of the hybrid DFA/NFA automaton that Vectorscan traverses internally. For a fixed set of patterns, this compilation only needs to happen once. The wrapper also provides database serialization/deserialization utilities, so it is easy to send/distribute that database over a network, or save to and load from a file.
Nevertheless, if the set of patterns changes, the database has to be recompiled from scratch. We recommend measuring both the database compilation time and the final in-memory size of the compiled database for your use-case. Generally speaking, the more patterns you compile and the higher their size and complexity, the higher the compilation time and memory requirements.
The database itself is never modified during the actual scanning, making it safe to use from multiple threads simultaneously. -
Creating a Scanner object from the compiled database: Each Scanner object allocates and maintains its own scratch space, which keeps track of its internal state required by the native Vectorscan engine. The native functions for scanning input can thus be kept completely allocation-free. The scratch space, and by extension the Scanner object, are modified during the scanning and are thus not safe to use from different threads simultanesouly.
-
Calling the Scanner object's scan() methods for fast, SIMD-powered scanning: A Scanner's scan() method requires two arguments:
- First, an object holding the payload for what you want to scan over. The wrapper supports Strings, byte arrays, ByteBuffers (both on-heap and direct), and MemorySegments (both heap and native).
- Second, a MatchHandler, which is a user-provided callback that gets executed on every match. Through that callback, the user can pass arbitrary logic to be executed on every match.
As a first example: Given a fixed set of literal patterns and an input String, we want to count how often each pattern exists inside the input:
@Test
void countLiteralPatternOccurrences() {
List<Expression> exprs = List.of(
new Expression("foo"),
new Expression("bar", EnumSet.of(Flags.CASELESS)),
new Expression("qux")
);
String input = "foo bar foo baz Bar foo";
int[] countsById = new int[exprs.size()];
try (Database database = new Database(exprs, ExecutionMode.BLOCK);
BlockScanner scanner = new BlockScanner(database)) {
MatchHandler countMatches = (id, from, to) -> {
countsById[id]++;
return true;
};
scanner.scan(input, countMatches);
}
for (int i = 0; i < exprs.size(); i++) {
IO.println(String.format("%s -> matched %d times.", exprs.get(i).pattern(), countsById[i]));
}
// foo -> matched 3 times.
// bar -> matched 2 times.
// qux -> matched 0 times.
}The list of Expressions specify the set of patterns we want to match. Every one of them is assigned an id
starting from 0, according to their position inside the List. A Database object gets instantiated by passing to it
those Expressions, and specifying an execution mode. A Scanner object then references the Database
(BlockScanner objects for Databases with Block execution mode, and StreamScanner objects for Databases with Stream execution mode).
For the scan method, you pass it an input plus an object implementing the MatchHandler interface. It receives 3 arguments:
- The id of the expression that matched.
- The byte position of where the match started. Note that by default, Vectorscan does not keep track of this value, unless the pattern flag SOM_LEFTMOST is enabled.
- The byte position of where the match ended. The return value of the callback specifies whether you want the scanning to continue: Returning true will continue the scan, while returning false will return immediately.
In this example, the provided MatchHandler increments the value inside an integer array, and then lets the scan continue.
As an advanced alternative to a Java-side MatchHandler, vectorscan4j also supports NativeMatchHandlers:
NativeMatchHandlers point to native functions that have to first be dynamically loaded during runtime.
Using native callback functions as opposed to Java functions can reduces the callback overhead that accrues with every upcall from native scanning to JVM execution.
For concrete usage examples, see the unit tests.
vectorscan4j provides utility methods for serializing and deserializing a Database object:
@Test
void serializeDeserializeDb() {
List<Expression> expressions = List.of(
new Expression("pat1"),
new Expression("pat2")
);
try (Database database = new Database(expressions, ExecutionMode.BLOCK)) {
byte[] dbBytes = database.serialize();
// ... save dbBytes to file, or send over network ...
// ...after e.g. reading bytes from a file, deserialize to Database.
Database roundTripDb = Database.deserialize(dbBytes);
roundTripDb.close();
}
}A small number of niche usage patterns can lead to suboptimal memory usage, or lead to undefined behavior.
Database- and Scanner objects both hold native memory, i.e. memory that is not managed by the JVM Runtime. Calling close() on those objects will clean the native memory that they are holding. This memory will also be freed if one doesn't call close() explicitly. However, in that case it will only happen when the Database- or Scanner object gets garbage collected (which might be far in the future).
// This function allocates off-heap memory and does not release it immediately
void createDb() {
// We create a new database, and don't call close() on it.
Database db = new Database(List.of(new Expression("a")), BLOCK_MODE);
// After this function returns, db goes out of scope, and will at some point be garbage collected.
// -> Its native memory gets freed, but not immediately.
}For more optimal memory usage, either manually close Database and/or Scanner objects by calling the close() method, or initialize them with a try-with-resources-block:
// This function is safe to call
void createDbSafe() {
try (Database db = new Database(List.of(new Expression("a")), BLOCK_MODE)) {
// Even if an exception gets thrown in here, JVM still closes db at the end of the try-block.
throw new RuntimeException("Something bad happened");
}
}When using the overloaded Scanner.scan(MemorySegment segment, ...) method, passing it a MemorySegment that points to a non-allocated region of memory leads to undefined behavior, including the possibility of segmentation faults.
try (Database db = new Database(List.of(new Expression("p1")), BLOCK_MODE);
BlockScanner scanner = new BlockScanner(db);
Arena arena = Arena.ofConfined()) {
MatchHandler doNothing = (id, from, to) -> true;
MemorySegment tiny = arena.allocate(10);
MemorySegment larger = tiny.reinterpret(200000L);
// This scan might cause a segmentation fault, breaking the JVM.
// If it doesn't cause a segmentation fault, it will scan over a random region of memory,
// leading to undefined behavior.
scanner.scan(larger, doNothing);
}Throwing exceptions inside the MatchHandler will always break the JVM, i.e. you can not catch that exception outside the scan call:
try (Database db = new Database(List.of(new Expression("p1")), BLOCK_MODE);
BlockScanner scanner = new BlockScanner(db)) {
MatchHandler throwException = (id, from, to) -> {
throw new RuntimeException("Hi, try and catch me!");
};
try {
scanner.scan("Here is p1!", throwException);
} catch(RuntimeException e) {
IO.println("We caught it!");
}
IO.println("We can continue execution.");
// -> "Unrecoverable uncaught exception encountered. The VM will now exit"
}This is due to how the JVM handles exceptions thrown during native upcalls back to Java code.
A Scanner object maintains an internal scratch space, which is not safe to use from multiple threads. Using the same Scanner from multiple threads will throw a VectorscanException.
Native Vectorscan supports multiple operating systems and CPU architectures (see the official repository).
At the moment, vectorscan4j targets Linux only, on x86-64 and ARM-based CPUs (tested on Ubuntu).
The Vectorscan engine provides only partial support for the following regex concepts:
- Capture groups
- Backreferences
- Look-arounds
By default, vectorscan4j rejects patterns that use those advanced regex features. If you enable the PREFILTER flag for those patterns, compilation can still succeed, but matching becomes approximate: scans may return false positives and should be followed by a secondary exact validation step if exact matches are required.
Currently, there is no support for Vectorscan's Vector mode execution.
This repository bundles prebuilt native binaries for Vectorscan under:
src/main/resources/native/linux/x86_64/libvectorscan.sosrc/main/resources/native/linux/aarch64/libvectorscan.so
The corresponding third-party notice and license reproduction is provided in:
THIRD_PARTY_NOTICES.mdsrc/main/resources/native/THIRD_PARTY_NOTICES.txt
This wrapper currently supports a core subset of Vectorscan's features, but not everything. If additional features are required, do not hesitate to raise issues or submit pull requests directly.