65.9K
CodeProject 正在变化。 阅读更多。
Home

rxcg: 用于 IoT 的简单文本匹配 C 代码生成器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2022 年 9 月 5 日

MIT

8分钟阅读

viewsIcon

16205

downloadIcon

203

生成紧凑的 C 代码以基于正则表达式匹配文本

rxcg

引言

更新:在大多数情况下生成更紧凑的代码。

更新 2:修复了 blockend 生成中的错误。

更新 3:将 XXXX_capture_t 的名称更改为 XXXX_match_t

更新 4:删除了常见类型(如 match_t)的命名空间前缀,并添加了几个命令行选项。改进了代码生成。

我写了这么多正则表达式引擎和它们的代码生成器,这几乎是我的本能了。我这样做是为了好玩。这一次,我这样做是因为它很有用。

我需要从 REST 式 Web 服务获得的 JSON 结果中提取单个字段。我需要在小型物联网设备上完成这项工作,而且我不想在整个 JSON 库中浪费宝贵的程序空间,而我只需要一小部分关键数据。

实际上,我需要的是正则表达式,但我也避免浪费空间在正则表达式引擎上。

最终,这意味着需要一个代码生成器,它能根据输入的正则表达式生成匹配器代码。通过使用这种工具,我可以获得正则表达式功能,但仅限于完成工作所需的功能,而在最终结果中不包含任何不必要的东西。与其将正则表达式馈送到 regex 引擎并在运行时进行处理和解释,不如使用已经针对硬编码的正则表达式进行优化的代码,而无需在运行时进行解释。

理解这段乱码

rxcg 是一个表驱动的 DFA/非回溯代码生成器。它生成的代码的复杂度为 O(n),其中 n 是输入字符串的长度。总而言之,这意味着在从左到右遍历流时,它最多只访问每个字符一次。

这也意味着它使用数据数组/表来驱动匹配,而不是硬编码的 goto。这在某些情况下会以速度为代价来换取大小,因为基于数组的技术可以减小闪存占用空间,但运行速度可能不如基于 goto 的编排。

这是一个朴实的设置,目的是使最终代码简单高效。它不支持子捕获组。您可以进行分组,但这只会改变运算符优先级。它不会将子捕获到组中。它不支持 ^$ 等锚点。但它确实支持 Unicode,一直到 UTF-32,以及常见的字符类,包括像 IsLetterIsDigit 这样的 Unicode 字符类。

为了灵活性,它使用一个回调函数来检索输入流中的下一个 UTF-32 代码点。您负责实现回调函数以检索您的数据,无论这些数据来自文件、字符串还是网络。这也意味着您需要负责将您的流转换为 UTF-32 代码点,无论您如何实现。幸运的是,生成器提供了这样的代码,并且我在示例中包含了从 UTF-8 或 ASCII 文件或字符串中执行此操作的代码。

输入文件采用 Reggie/Rolex 文件格式。

它是一种基于行的格式,每行类似于以下内容

CIdent<ignoreCase>= '[A-Z_][0-9A-Z_]*'

或者更普遍地表示为 EBNF

identifier [ "<" attributes ">" ] "=" ( "'" regexp "'" | "\"" literal "\"" )

每个属性都是一个标识符,后面可以选择性地跟着 = 和一个 JSON 风格的字符串、布尔值、整数或其他值。如果未指定值,则为 true,这意味着 ignoreCase 设置为 true。请注意,字面量表达式用双引号括起来,而正则表达式用单引号括起来!

有几个可用的属性,如下所示

  • ignoreCase - 表示表达式应被解释为不区分大小写
  • blockEnd - 表示指定符号的多字符结束条件。遇到时,匹配器将继续读取直到找到 blockEnd 值,并将其消耗掉。这对于具有多字符结束条件的表达式很有用,例如 C 块注释、HTML/SGML 注释和 XML CDATA 部分。请记住,如果 blockEnd 是字符串,请使用双引号;如果它是正则表达式,请使用单引号。

如上所述,正则表达式语言支持基本的非回溯构造和常见的字符类,以及 [:IsLetter:]/[:IsLetterOrDigit:]/[:IsWhiteSpace:]/等等,它们映射到 .NET 的对应项。请务必转义单引号,因为它们在文件中用于分隔表达式。

该工具是一个基于命令行的应用程序,它接受一个输入文件和一个可选的 /size <capture_size> 参数,并从中生成两个文件:Name.hName.c,其中 Name 是输入文件的名称。对于 MyMatch.rl,该工具将生成 MyMatch.hMyMatch.c

/size 参数表示捕获缓冲区的大小。在匹配时,会返回匹配文本的代码点,但它们不能大于指定的大小,否则捕获将被截断。这可以是一个数值,也可以是已定义的 #define 宏的名称,例如 CAPTURE_SIZE

代码生成器本身是用 C# 编写的,并使用了我的 FastFA 正则表达式引擎,该引擎也用于我的另一个项目 Reggie。事实上,我无耻地借鉴了该项目的大量代码来用于此项目,因此请访问该链接以获取概述。我本可以编写一个 Reggie 插件来完成这项工作,但老实说,Reggie 对此来说有点过于笨重,而且我想对其进行一些简化,特别是对于物联网而言,执行词法分析等操作是适得其反的。

使用这个烂摊子

命令行工具很简单。让我们从使用屏幕开始

Usage: rxcg.exe <input> [/size <capture_size>] [/ifstale] [/stdint] [/prefix]

<input>        The input file to use
<capture_size> The size of the capture
               buffer. Default is 256
<ifstale>      Only generate if source
               is newer than target
<prefix>       Generate a prefix for
               the match struct
<stdint>       Use <stdint.h>

第一个参数是必需的,它是 Rolex 词法分析器格式的输入文件,如上所述。其余参数是可选的

  • <capture_size> 可以是字面值,也可以是预处理器宏,用于指定捕获缓冲区的大小。
  • <ifstale> 将跳过源文件的生成,除非输入比输出新。
  • <prefix> 将文件名前置于常见名称(如 match_t)之前,就像以前的版本那样。
  • <stdint> 将导致生成的代码使用 <stdint.h> 作为其数值类型。

编写这个混乱的程序

使用生成的代码首先涉及进行回调。如果您只关心 7 位 ASCII,这很容易,您可以通过将每个 ASCII 字符强制转换为 int32_t 来作弊,但对于 Unicode 支持,您需要付出一些实际的努力。

示例项目包含两个基本的回调实现 - 一个用于字符串,一个用于文件。在每种情况下,我们需要跟踪输入以及我们在其中的位置,以便在每次被调用时返回下一个字符,或者在到达末尾时返回 -1。这些实现也作为生成代码的一部分提供,但被 #if 0 掉了。

这是字符串实现

// holds the current string pointer
typedef struct string_cb_state {
    char* sz;
} string_cb_state_t;

int32_t string_callback(
        unsigned long long* out_advance, 
        void* state) {
    string_cb_state_t* ps = (string_cb_state_t*)state;
    int32_t cp;
    if (!*ps->sz) {
        // end of stream
        *out_advance = 0;
        return -1;
    }
    // decode UTF-8 to UTF-32
    uint8_t byte = (uint8_t)*ps->sz;
    if ((byte & 128) == 0) {
        cp = ((uint32_t) *ps->sz & ~128);
        *out_advance = 1;
    }

    if ((byte & 224) == 192) {
        cp=((uint32_t) ps->sz[0] & ~224) << 6 |
        ((uint32_t) ps->sz[1] & ~192);
        *out_advance = 2;
    }

    if ((byte & 240) == 224) {
        cp=((uint32_t) ps->sz[0] & ~240) << 12 |
        ((uint32_t) ps->sz[1] & ~192) << 6 |
        ((uint32_t) ps->sz[2] & ~192);
        *out_advance = 3;
    }

    if ((byte & 248) == 240) {
        cp=((uint32_t) ps->sz[0] & ~248) << 18 |
                ((uint32_t) ps->sz[1] & ~192) << 12 |
                ((uint32_t) ps->sz[2] & ~192) << 6 |
                ((uint32_t) ps->sz[3] & ~192);
        *out_advance = 4;
    }
    // advance the pointer by
    // what we consumed
    ps->sz+=*out_advance;
    
    return cp;
}

大部分混乱在于处理 Unicode。除此之外,我们所做的只是在 state(类型为 string_cb_state_t)中推进指针,并在完成后返回 -1

文件实现类似,尽管在某些方面更简单,因为 state 只是一个 FILE 句柄,它会跟踪并推进自己的光标。请注意,在 Arduino 上,您会使用 File 对象并与之一起工作,但示例是标准的 C。

int32_t file_callback(
        unsigned long long* out_advance, 
        void* state) {
    FILE* h = (FILE*)state;
    int32_t cp;
    int i = fgetc(h);
    if (i==-1) {
        *out_advance = 0;
        return -1;
    }
    uint8_t byte = (uint8_t)i;
    if ((byte & 128) == 0) {
        cp = ((uint32_t) i & ~128);
        *out_advance = 1;
    }

    if ((byte & 224) == 192) {
        cp=((uint32_t) i & ~224) << 6 |
        ((uint32_t) fgetc(h) & ~192);
        *out_advance = 2;
    }

    if ((byte & 240) == 224) {
        cp=((uint32_t) i & ~240) << 12 |
        ((uint32_t) fgetc(h) & ~192) << 6 |
        ((uint32_t) fgetc(h) & ~192);
        *out_advance = 3;
    }

    if ((byte & 248) == 240) {
        cp=((uint32_t) i & ~248) << 18 |
                ((uint32_t) fgetc(h) & ~192) << 12 |
                ((uint32_t) fgetc(h) & ~192) << 6 |
                ((uint32_t) fgetc(h) & ~192);
        *out_advance = 4;
    }
    return cp;
}

在字符串实现之后,这个实现应该很明显了。

请注意,所有这些例程都假定流是 7 位 ASCII 或有效的 UTF-8。没有进行检查,因此可能会发生非常糟糕的事情™,特别是当字符串回调函数收到无效流时。

现在让我们探讨一下如何使用它。

对于输入文件中的每个命名正则表达式,都会生成一个名为 match_Name() 的函数,其中 Name 是输入文件中 = 左侧的名称。

考虑 Example.rl 文件中的以下行

Identifier = '[_[:IsLetter:]][_[:IsLetterOrDigit:]]*'

这将生成一个名为 match_Identifier() 的函数,然后可以使用它来查找匹配该 Unicode 表达式的字符串。

int main(int argc, char** argv) {
    char* test = 
        "a1234 foobar /*5678 abc123 */ -";
    unsigned long long pos = 0;
    string_cb_state_t st;
    st.sz = test;
    while (1) {
        match_t c = 
            match_Identifier(&pos, 
                            string_callback, 
                            &st);
        if (0 == c.length) {
            // empty capture indicates end
            return 0;
        }
        // print the capture buffer
        // (assumes ASCII)
        for(int i = 0;i<c.length;++i) {
            printf("%c",(char)c.capture[i]);
        }
        printf("\n");
    }
}

这里的想法是反复调用匹配函数,每次都获取一个新的 match_t,直到返回一个 length 为 0 的值,这表示已到达输入末尾。

请注意,捕获缓冲区本身由 int32_t 值组成,而不是人们可能期望的 char。此匹配器捕获的是 UTF-32 代码点,而不是字符。这解释了代码末尾循环并打印 capture 内容的部分。另请注意,捕获缓冲区不是以 null 结尾的。

注意:如果您想在 Arduino 框架中使用此代码,应在编译器的命令行上定义 ARDUINO 为一个常量。或者,修改生成的头文件以无条件地 #include <Arduino.h>,或者只是确保它在 Arduino.h 之后包含。

关注点

此代码生成容纳 DFA 数组所需的最小元素类型。这意味着如果它是 ASCII 表达式且状态机足够小,它将使用 int8_t 作为数组元素类型。否则,它将根据需要使用 int16_tint32_t。每个数组类型使用一个匹配器例程。这意味着将生成 1 到 3 个匹配器实现来处理所有匹配。它们都几乎相同,以下是它们的样子,并包含 blockend 代码

(请原谅换行,这个例程很长很深。)

match_t Example_runner32(int32_t* dfa, int32_t* blockEnd, 
    unsigned long long* position, Example_callback callback, void* callback_state) {
    match_t result;
    result.position = 0;
    result.length = 0;
    unsigned long long adv = 0;
    int tlen;
    int32_t tto;
    int32_t prlen;
    int32_t pmin;
    int32_t pmax;
    int32_t i, j;
    int32_t ch;
    int32_t state = 0;
    int32_t acc = -1;
    int done;
    unsigned long long cursorPos = *position;
    ch = callback(&adv, callback_state);
    while (ch != -1) {
        result.length = 0;
        result.position = cursorPos;
        acc = -1;
        done = 0;
        while (!done) {
start_dfa:
            done = 1;
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
            acc = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
            tlen = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
#else
            acc = dfa[state++];
            tlen = dfa[state++];
#endif
            for (i = 0; i < tlen; ++i) {
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
                tto = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
                prlen = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
#else
                tto = dfa[state++];
                prlen = dfa[state++];
#endif
                for (j = 0; j < prlen; ++j) {
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
                    pmin = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
                    pmax = (int32_t)pgm_read_dword((uint32_t*)(dfa + (state++)));
#else
                    pmin = dfa[state++];
                    pmax = dfa[state++];
#endif
                    if (ch < pmin) {
                        break;
                    }
                    if (ch <= pmax) {
                        if (result.length < 256) {
                            result.capture[result.length++] = ch;
                        }
                        ch = callback(&adv, callback_state);
                        ++cursorPos;
                        state = tto;
                        done = 0;
                        goto start_dfa;
                    }
                }
            }
            if (acc != -1) {
                if (blockEnd != NULL) {
                    state = 0;
                    while (ch != -1) {
                        acc = -1;
                        done = 0;
                        while (!done) {
start_block_end:
                            done = 1;
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
                            acc = (int32_t)pgm_read_dword((uint32_t*)
                                  (blockEnd + (state++)));
                            tlen = (int32_t)pgm_read_dword((uint32_t*)
                                   (blockEnd + (state++)));
#else
                            acc = blockEnd[state++];
                            tlen = blockEnd[state++];
#endif
                            for (i = 0; i < tlen; ++i) {
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
                                tto = (int32_t)pgm_read_dword((uint32_t*)
                                      (blockEnd + (state++)));
                                prlen = (int32_t)pgm_read_dword((uint32_t*)
                                        (blockEnd + (state++)));
#else
                                tto = blockEnd[state++];
                                prlen = blockEnd[state++];
#endif
                                for (j = 0; j < prlen; ++j) {
#if defined(ARDUINO) && !defined(CORE_TEENSY) && !defined(ESP32)
                                    pmin = (int32_t)pgm_read_dword((uint32_t*)
                                           (blockEnd + (state++)));
                                    pmax = (int32_t)pgm_read_dword((uint32_t*)
                                           (blockEnd + (state++)));
#else
                                    pmin = blockEnd[state++];
                                    pmax = blockEnd[state++];
#endif
                                    if (ch < pmin) {
                                        break;
                                    }
                                    if (ch <= pmax) {
                                        if (result.length < 256) {
                                            result.capture[result.length++] = ch;
                                        }
                                        ch = callback(&adv, callback_state);
                                        ++cursorPos;
                                        state = tto;
                                        done = 0;
                                        goto start_block_end;
                                    }
                                }
                            }
                        }
                        if (acc != -1) {
                            return result;
                        }
                        else {
                            if (result.length < 256) {
                                result.capture[result.length++] = ch;
                            }
                            ch = callback(&adv, callback_state);
                            ++cursorPos;
                        }
                        state = 0;
                    }
                    state = 0;
                    continue;
                }
                else {
                    if (result.length > 0) {
                        return result;
                    }
                }
            }
            ch = callback(&adv, callback_state);
            ++cursorPos;
            state = 0;
        }
    }
    result.length = 0;
    return result;
}

它作为一个有趣的奇观提供给有兴趣的读者。请注意,blockend 部分仅在被使用时生成。总之,我们不会详细探讨它,因为它遍历从正则表达式生成的数组,并且当这些数组被编译时,所有有趣的逻辑都已内置其中。遍历它很有效,但并不直观。

历史

  • 2022 年 9 月 5 日 - 初次提交
  • 2022 年 9 月 15 日 - 通过消除未使用的 blockend 代码使代码生成更紧凑
  • 2022 年 9 月 15 日 - 修复了为 blockend 生成正则表达式匹配数组的错误
  • 2022 年 9 月 16 日 - 稍微更改了命名
  • 2023 年 11 月 15 日 - 添加了几项功能并更改了命名
© . All rights reserved.