Introduction

vProto - is an advanced description of regular expressions, allowing for the specification of a Finite State Machine, including states, behaviors, and transition conditions. This description serves as the basis for generating C++ code, tailored for data parsing purposes.

State Machine descriptions are done using tokens. Tokens written on a single line denote sequential traversal (once one is completed, it moves to the next). Branching occurs when transitioning to a new line with an increase in depth (increase in indentation). Lines at the same depth correspond to branches. At the end of a line, there may be the presence of the symbol \ which symbolizes continuation of the line (including possible branching), and it will be used during transition in case branching is terminated.

Example:
    token_1_1 token_1_2 token_1_3
        token_2_1 token_2_2 \
            token_3_1 token_3_2
            token_4_1 token_4_2
        token_5_1 token_5_2
        token_6_1 token_6_2
    token_7_1 token_7_2 token_7_3

Example (Example → 'Generate' → scroll down to the state machine at the bottom)
Tokens written on the same line are executed sequentially, one after another (token_1_1 → token_1_2 → token_1_3)
When moving to the next line and increasing the depth (by one tab), branching occurs between tokens that are at the same depth:

During branching, the branch is selected using a greedy algorithm (the first suitable branch), unless it is specified to use parallel states: < >.
In the given example, the tokens token_1_1 and token_7_1 are at the same depth (branch1), so effectively a branching occurs between them. Let's assume a transition to token_1_1 -> token_1_2 -> token_1_3 will be executed sequentially, one after the other, since they are on the same line. After the token token_1_3, branching occurs to token_2_1 token_5_1 token_6_1 (because they are at the same depth, branch2), accordingly, the execution priority is from top to bottom. But we will no longer be able to return to the earlier branching between token_1_1 token_7_1, because after completing token_1_3 the \ symbol is absent. Let’s assume there is a transition to the branch token_2_1 -> token_2_2. After completing token_2_2, branching occurs to the branches token_3_1 token_4_1. However, after completing them, there will be a return to the branching between token_2_1, token_5_1, token_6_1, because after token_2_2 there is a \ symbol. But because the \ symbol is absent after token_1_3, so we do not return to the higher level.In fact, the \ symbol indicates a continuation of the line (with possible branching) and necessarily a transition to a lower level (one more Tab character).

The successful passage of a token (and consequently, transition to the next) depends on the incoming data. Sometimes tokens can be optional or have certain requirements for their repetition. It's possible to modify token traversal properties by adding additional options using parentheses, for example: (min=5, max=10, init=100).

Shorter forms can also be used: ? (equivalent to min=0, max=1), * (equivalent to min=0, max=infinity), + (equivalent to min=1, max=infinity).

If incoming characters do not match the token, an exception will occur (preventing a transition to the next token), which can be caught; otherwise, parsing will exit. To catch the exception, you need to add a branch with 'catch:' specified. If a '\' character exists in the branch, it will be analyzed as part of the parent branch catch.

Specialized tokens (such as call, jmp, etc.) can be used to allow transitions between branches or to create multiple trees. It is recommended to review the examples in the top-right corner.
Using parallel states <> allows branching to occur in parallel. The first branching token that is fully processed will be used as the single active state, and the others will be discarded.

Range [a-zA-Z\x20\r\n]

Describes the range of permissible values for the incoming byte. The expected format is equivalent to standard range in regular expressions. This token supports:

Also, this token supports redirection to a variable, meaning that while we are within this token, all data will be redirected to the specified variable (equivalent min=1, max=infinity). It is possible to use modifiers (string/string_view/array/uint/hex/bool/bin(binLe)/binBe) that will affect the variable initialization and assignment.

Variables u8-64, h8-64, be8-64, le8-64, array, string, string_view, bool, enum

As mentioned earlier, when initializing variables, the main type is a range, which can describe all variables. However, there are also shorter forms of variable notation:

If you do not specify the ':', redirection to the variable will not occur, but the data will be ignored, equivalent to range without redirection.
Example of TLV parsing: b8:type be32:length data:value(max=length)
Example of UDP header parsing: be16:srcPort be16:dstPort be16:dataLength be16:checksum data:udpPayload(max=dataLength)

"casesensitive text" or 'no-casesensitive'

"casesensitive" or 'no-casesensitive' - describes constant words or binary values. For example, in the HTTP protocol, words like "GET" or "HTTP/1.1" are case-sensitive, while headers like 'Content-Length' or 'Content-type' can be written using uppercase or lowercase characters. You can also describe the binary value of a character, like in ranges.

{ c++ user code } notify:userFunction if{c++condition}

{ c++ code } or notify:userFunction - calling user's C++ code, this token doesn't use incoming bytes but can be used for modifying variables, invoking callbacks, determining branching conditions, or during debugging stages. This code is called as a function, and inside it, the user can "return false" (by default, it automatically returns true) - indicating successful or unsuccessful token processing.
if {condition} is recommended for use if it is placed at the beginning of the branching, the condition in parentheses is the defining one, and the use of the word "return" must be absent.
Examples:
b8:type { printf("Read Type: %u ", type); }
    if {type == 1} notify:userFunction1
    if {type == 2} notify:userFunction2
    if {type == 3} notify:userFunction3
C++ insertion is used for debugging\printf function (automatically setting successful token processing return true), as well as for branching: the "return type == ..." returns true or false, thereby selecting a branch transition.

If a user wishes to add their own functions or repositories, they need to define the "OutputClassName". The parser will inherit from this class and access its variables and functions. It is recommended to inherit OutputClassName from the demoResult state structure.

notify:userFunction - essentially replaces the C++ insert { userFunction(); }, but shorter and more efficient in terms of performance. Also, you can pass parameters like notify:userFunction(var1, var2, ...), but don’t forget to manually declare this function in the outputName class.
Example TLV data struct: b8:type be32:length [\x00-\xff]->string:value(max=length) notify:gotValue

begin:userFunction end:userFunction

begin:userFunction end:userFunction - transfers to the user all data between the beginning and the end.
Example(will return to the user all the data that was used in the tokens between begin and end):
"GET" [ \t]+ begin:userFunction [^ \t]+ end:userFunction [ \t]+ "HTTP/" [0-9] "." [0-9] "\r"? "\n"

label:name, jmp:name, call:name, return

This tokens are used for transitions within the state tree:

Example:
call:readRequestType call:readUrl call:readHeaders

label:readRequestType
    "GET" return
    "POST" return

label:readUrl [ \t]+ [^ \t]->string:url [ \t]+ "HTTP/" [0-9] "." [0-9] "\r"? "\n" return

label:readHeaders ...

reset, break, bang, back:depthNumber

Example:
'A' // depth == 0
    'B' // depth == 1
        'C' back:1 // depth == 2, back:1 go to depth == 1, between 'B' and 'D'.
    'D' // depth == 1
        'E' back:0 // depth == 2, back:0 go to depth == 0, between 'A' and 'F'.
'F' // depth == 0

<tokensCase1, tokensCase2, tokensCase3>

<listTokensCase1, listTokensCase2,..> - this token allows creating nested branching variations within itself, describing a sequence of regular tokens and comma-separated alternative options. Its presence changes the logic of the entire state machine by creating parallel states, enabling simultaneous processing across multiple states. Completion of any sequence leads to the termination of other parallel states, essentially, the 'bang' token is automatically set after this token. This token significantly simplifies understanding of the finite state machine, but working in parallel states is always slower than working in a single state. It is recommended to minimize the description of variations in this token so that options are discarded as quickly as possible.
Example:
<"GET", "POST", "PUT"> - All three states are processed in parallel until one of them wins. If the byte G is received, then only the GET branch has a chance of passing; POST and PUT automatically fail. But if the byte P is received, then we will be simultaneously in the POST and PUT states, waiting for other symbols to determine the state.
Example (parallel states):
"GET" [ \t]+ \
    <'url-1'> [ \t]+
    <'url-2'> [ \t]+
    <'url-3'> [ \t]+
All three states url-1, url-2, url-3 are considered in parallel as data arrives (which can come byte by byte). Decisions are made about which states to exclude gradually.
Example (no parallel states):
"GET" [ \t]+ \
    'url-1' [ \t]+
    'url-2' [ \t]+
    'url-3' [ \t]+
Without using <>, branching will not be considered as parallel states, and transitions to url-2 and url-3 will not be possible because upon the arrival of the symbol 'u', the transition to the first, more prioritized branch url-1 (because it first) will be chosen, and if, for example, a url-2 comes next, the transition to url-2 will not occur. An exception will occur at the utl-1 branch.

vproto:moduleName:varName

vproto:moduleName:varName - the ability to delegate parsing to other vproto modules. Example: an initial parsing module handles protocols such as IMAP, POP3, or SMTP, and then passes the data to the common MIME module for further parsing. Another example is PCAP parsing, which consists of a separate module for reading the PCAP header (first module):
"\xd4\xc3\xb2\xa1" data(max=20)
    le32:ts_sec le32:ts_usec le32:pktLen le32 vproto:packet:var(max=pktLen)
followed by a module for parsing individual packets (second module name "packet"). For example:
array:macDst(max=6, init=6) array:macSrc(max=6, init=6) be16:protoL2 \
    { return protoL2 == 0x0800; // ipv4} ...
    { return protoL2 == 0x86dd; // ipv6} ...
Further parsing of the extracted data can also be delegated to subsequent modules. It is recommended to use it in combination with parameters like (max=length) or with ranges such as [a-z]->vproto:moduleName:varName

Optimization & Performance

Special attention is given to the performance of the generated code, even without special instructions (which exist in x86-64), to ensure code universality and its operation across multiple platforms.

Recommendations for achieving maximum performance:

Performance Testing involves running the most typical GET request (430 bytes) looped 100 million times (43 GB total), with all processing done on a SINGLE CORE IN ONE THREAD. Comparison is made against a modified description of the HTTP protocol (manual parsing of parallel states) and the Boost::HTTP library. The Valgrind report is also attached for a loop of 1 million iterations
vProtovProto (SSE+AVX)boost-1.85::http
Jetson Orin (ARM-8 rev1 2200mhz)11.51gbit/s (3.34m req/s)doesn't support3.43gbit/s (997.09k req/s)
AMD Ryzen9 5950X13.28gbit/s (3.86m req/s)15.60gbit/s (4.53m req/s)5.19gbit/s (1.50m req/s)
Intel Xeon W-222313.74gbit/s (3.99m req/s)25.50gbit/s (7.41m req/s)4.13gbit/s (1.20m req/s)
Intel Xeon Gold 634816.85gbit/s (4.89m req/s)26.30gbit/s (7.64m req/s)5.44gbit/s (1.58m req/s)
Intel i7-1065G718.53gbit/s (5.38m req/s)27.00gbit/s (7.84m req/s)4.45gbit/s (1.29m req/s)
AMD EPYC 9454P21.14gbit/s (6.14m req/s)31.30gbit/s (9.09m req/s)6.10gbit/s (1.77m req/s)
AMD Ryzen7 8845hs29.80gbit/s (8.66m req/s)41.40gbit/s (12.03m req/s)5.48gbit/s (1.59m req/s)
Intel i9-12900k31.22gbit/s (9.07m req/s)34.50gbit/s (10.02m req/s)9.88gbit/s (2.87m req/s)
Intel i9-13900hx27.50gbit/s (7.99m req/s)41.70gbit/s (12.12m req/s)9.73gbit/s (2.82m req/s)
AMD Ryzen AI 9 HX 37041.20gbit/s (11.97m req/s)55.10gbit/s (16.01m req/s)11.80gbit/s (3.43m req/s)
AMD Ryzen 9 9950X3D43.70gbit/s (12.70m req/s)59.30gbit/s (17.23m req/s)13.60gbit/s (3.95m req/s)
The same test, but after using profiling:
vProtovProto (SSE+AVX)boost-1.85::http
Jetson Orin (ARM-8 rev1 2200mhz)13.57gbit/s (3.94m req/s)doesn't support4.42gbit/s (1.28m req/s)
AMD Ryzen9 5950X13.77gbit/s (4.00m req/s)16.30gbit/s (4.73m req/s)6.51gbit/s (1.89m req/s)
Intel Xeon W-222317.72gbit/s (5.15m req/s)29.30gbit/s (8.51m req/s)5.11gbit/s (1.48m req/s)
Intel Xeon Gold 634818.63gbit/s (5.41m req/s)32.80gbit/s (9.53m req/s)5.91gbit/s (1.71m req/s)
Intel i7-1065G719.91gbit/s (5.78m req/s)36.50gbit/s (10.61m req/s)5.32gbit/s (1.54m req/s)
AMD EPYC 9454P25.73gbit/s (7.47m req/s)44.20gbit/s (12.84m req/s)8.73gbit/s (2.53m req/s)
AMD Ryzen7 8845hs33.23gbit/s (9.65m req/s)57.60gbit/s (16.74m req/s)6.87gbit/s (1.99m req/s)
Intel i9-12900k39.67gbit/s (11.53m req/s)57.60gbit/s (16.74m req/s)11.47gbit/s (3.33m req/s)
Intel i9-13900hx40.53gbit/s (11.78m req/s)63.70gbit/s (18.51m req/s)11.59gbit/s (3.36m req/s)
AMD Ryzen AI 9 HX 37049.10gbit/s (14.27m req/s)69.20gbit/s (20.11m req/s)11.80gbit/s (3.43m req/s)
AMD Ryzen 9 9950X3D44.20gbit/s (12.84m req/s)72.00gbit/s (20.93m req/s)14.78gbit/s (4.29m req/s)
Another test compares the generated JSON code (from example) with RapidJSON. The test uses a typical JSON of small size, 796 bytes. A distinguishing feature of JSON compared to HTTP is the constant transitions between states, whereas in the case of HTTP, we spend the majority of time in a single state. In this example, used with relatively short data, the generated JSON code runs about 10% faster with SSE (RapidJSON also utilizes SSE). If the fields in JSON are longer, the contribution to performance from SSE will be even greater. The Valgrind report is also attached for a loop of 1 million iterations
jsonFlowjsonPerfrapidJson-v1.1.0
Jetson Orin (ARM-8 rev1 2200mhz)4.91gbit/s (771.04k req/s)8.73gbit/s (1.37m req/s)4.11gbit/s (645.41k req/s)
AMD Ryzen9 5950X6.10gbit/s (957.91k req/s)16.20gbit/s (2.54m req/s)6.33gbit/s (994.03k req/s)
Intel Xeon W-22237.31gbit/s (1.14m req/s)9.13gbit/s (1.43m req/s)4.88gbit/s (766.33k req/s)
Intel Xeon Gold 63487.18gbit/s (1.12m req/s)11.14gbit/s (1.74m req/s)5.83gbit/s (915.51k req/s)
Intel i7-1065G77.54gbit/s (1.18m req/s)11.20gbit/s (1.75m req/s)8.45gbit/s (1.32m req/s)
AMD EPYC 9454P8.13gbit/s (1.27m req/s)14.72gbit/s (2.31m req/s)9.57gbit/s (1.50m req/s)
AMD Ryzen7 8845hs11.00gbit/s (1.72m req/s)19.77gbit/s (3.10m req/s)9.44gbit/s (1.48m req/s)
Intel i9-12900k11.09gbit/s (1.74m req/s)18.41gbit/s (2.89m req/s)12.29gbit/s (1.92m req/s)
Intel i9-13900hx13.22gbit/s (2.07m req/s)22.64gbit/s (3.55m req/s)13.27gbit/s (2.08m req/s)
AMD Ryzen AI 9 HX 37017.70gbit/s (2.77m req/s)26.60gbit/s (4.17m req/s)14.10gbit/s (2.21m req/s)
AMD Ryzen 9 9950X3D17.90gbit/s (2.81m req/s)26.81gbit/s (4.21m req/s)15.60gbit/s (2.44m req/s)
The same test, but after using profiling:
jsonFlowjsonPerfrapidJson-v1.1.0
Jetson Orin (ARM-8 rev1 2200mhz)5.14gbit/s (807.16k req/s)9.25gbit/s (1.45m req/s)2.93gbit/s (460.11k req/s)
AMD Ryzen9 5950X6.07gbit/s (953.20k req/s)18.60gbit/s (2.92m req/s)5.26gbit/s (826.00k req/s)
Intel Xeon W-22237.47gbit/s (1.17m req/s)11.52gbit/s (1.80m req/s)4.16gbit/s (653.26k req/s)
Intel Xeon Gold 63487.92gbit/s (1.24m req/s)12.48gbit/s (1.95m req/s)4.13gbit/s (648.55k req/s)
Intel i7-1065G78.70gbit/s (1.36m req/s)12.67gbit/s (1.98m req/s)4.77gbit/s (749.05k req/s)
AMD EPYC 9454P9.84gbit/s (2.86m req/s)17.10gbit/s (4.97m req/s)5.45gbit/s (1.58m req/s)
AMD Ryzen7 8845hs11.20gbit/s (1.75m req/s)22.70gbit/s (3.56m req/s)6.13gbit/s (962.62k req/s)
Intel i9-12900k14.20gbit/s (2.22m req/s)24.55gbit/s (3.85m req/s)8.50gbit/s (1.33m req/s)
Intel i9-13900hx16.10gbit/s (2.52m req/s)27.13gbit/s (4.26m req/s)7.02gbit/s (1.10m req/s)
AMD Ryzen AI 9 HX 37017.90gbit/s (2.81m req/s)27.50gbit/s (4.31m req/s)8.95gbit/s (1.40m req/s)
AMD Ryzen 9 9950X3D19.10gbit/s (2.99m req/s)29.32gbit/s (4.60m req/s)10.61gbit/s (1.66m req/s)

Сooperation

My collection of parsed protocols (via vProto) includes:

I am open to cooperation or participating in projects.

Author: Shchekoldin Sergey (Щеколдин Сергей)
shchekoldin@gmail.com