20、正则表达式:POSIX regex与Windows的regex库(如pcre2)的跨平台封装

正则表达式这东西,说实话,用起来爽,跨平台起来就有点头疼了。我在做跨平台工具链的时候,就踩过这个坑——Linux上跑得好好的正则,到了Windows上直接罢工。原因很简单:两个平台用的正则库根本不是一回事。

Linux/Unix世界默认用的是POSIX regex,而Windows那边,要么用自家的std::regex(性能嘛...你懂的),要么用PCRE2这种第三方库。今天我们就聊聊怎么把它们封装起来,让代码在两边都能跑得顺。

先搞清楚:POSIX regex和PCRE2到底差在哪?

我刚开始做封装时,以为不就是个正则匹配嘛,能有多大区别?结果一上手就发现,这俩的API设计哲学完全不同。

特性 POSIX regex PCRE2
头文件 regex.h pcre2.h
编译函数 regcomp() pcre2_compile()
匹配函数 regexec() pcre2_match()
释放资源 regfree() pcre2_code_free()
语法支持 基本ERE/ERE Perl兼容,支持前瞻/后顾
线程安全 需自行加锁 原生支持

你看,连函数名都不一样。更坑的是,POSIX regex的错误码是int,PCRE2的错误码是int但含义完全不同。嗯,这里要特别注意。

核心思路:抽象接口 + 条件编译

封装的核心就两招:抽象出一套统一的接口,然后用预处理宏来区分平台。我个人习惯把接口定义成regex_tregex_compile()这种风格,这样上层代码完全不用关心底层是谁。

关键原则:上层代码只调用你封装的接口,永远不要直接调用regcomp()pcre2_compile()。否则,跨平台就是个笑话。

我画了一张图,帮你理清这个封装层次:

应用层代码 统一抽象接口 regex_compile() / regex_match() / regex_free() POSIX regex 实现 regcomp() / regexec() / regfree() PCRE2 实现 pcre2_compile() / pcre2_match() / pcre2_code_free() #ifdef __linux__ #ifdef _WIN32

动手写封装:一个最小可用的跨平台正则库

废话不多说,直接上代码。这是我封装的一个精简版,核心就三个函数:编译、匹配、释放。

// regex_wrapper.h
#ifndef REGEX_WRAPPER_H
#define REGEX_WRAPPER_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

// 统一的正则对象类型
typedef struct {
    void *impl;      // 指向底层实现
    int   last_error;
} regex_t;

// 匹配结果
typedef struct {
    int    matched;
    size_t start;
    size_t end;
} regex_match_t;

// 统一接口
int  regex_compile(regex_t *re, const char *pattern);
int  regex_match(regex_t *re, const char *str, regex_match_t *result);
void regex_free(regex_t *re);

#ifdef __cplusplus
}
#endif

#endif // REGEX_WRAPPER_H

你看,这个头文件里没有任何平台相关的代码。上层调用者只需要知道regex_compileregex_matchregex_free这三个函数就够了。

实现层:两套代码,一个接口

下面是Linux上的POSIX实现。说实话,POSIX regex的API有点老派,但胜在稳定。

// regex_wrapper_posix.c
#include "regex_wrapper.h"
#include <regex.h>
#include <stdlib.h>
#include <string.h>

int regex_compile(regex_t *re, const char *pattern) {
    regex_t *posix_re = (regex_t *)malloc(sizeof(regex_t));
    if (!posix_re) return -1;

    int ret = regcomp(posix_re, pattern, REG_EXTENDED);
    if (ret != 0) {
        free(posix_re);
        return ret;
    }

    re->impl = posix_re;
    re->last_error = 0;
    return 0;
}

int regex_match(regex_t *re, const char *str, regex_match_t *result) {
    regex_t *posix_re = (regex_t *)re->impl;
    regmatch_t pmatch[1];

    int ret = regexec(posix_re, str, 1, pmatch, 0);
    if (ret == 0) {
        result->matched = 1;
        result->start = pmatch[0].rm_so;
        result->end   = pmatch[0].rm_eo;
    } else {
        result->matched = 0;
    }

    return ret;
}

void regex_free(regex_t *re) {
    if (re && re->impl) {
        regfree((regex_t *)re->impl);
        free(re->impl);
        re->impl = NULL;
    }
}

Windows上用PCRE2的实现,逻辑一样,但函数名完全不同:

// regex_wrapper_pcre2.c
#include "regex_wrapper.h"
#include <pcre2.h>
#include <stdlib.h>

int regex_compile(regex_t *re, const char *pattern) {
    int errcode;
    PCRE2_SIZE erroffset;

    pcre2_code *code = pcre2_compile(
        (PCRE2_SPTR)pattern,
        PCRE2_ZERO_TERMINATED,
        0,
        &errcode,
        &erroffset,
        NULL
    );

    if (!code) return errcode;

    re->impl = code;
    re->last_error = 0;
    return 0;
}

int regex_match(regex_t *re, const char *str, regex_match_t *result) {
    pcre2_code *code = (pcre2_code *)re->impl;
    pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(code, NULL);

    int rc = pcre2_match(
        code,
        (PCRE2_SPTR)str,
        PCRE2_ZERO_TERMINATED,
        0,
        0,
        match_data,
        NULL
    );

    if (rc >= 0) {
        PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
        result->matched = 1;
        result->start = ovector[0];
        result->end   = ovector[1];
    } else {
        result->matched = 0;
    }

    pcre2_match_data_free(match_data);
    return rc;
}

void regex_free(regex_t *re) {
    if (re && re->impl) {
        pcre2_code_free((pcre2_code *)re->impl);
        re->impl = NULL;
    }
}

条件编译:让构建系统自动选择

有了两套实现,怎么让编译器自动选?很简单,用#ifdef或者让CMake来决定编译哪个源文件。

// CMakeLists.txt 片段
if(UNIX)
    target_sources(myapp PRIVATE regex_wrapper_posix.c)
elseif(WIN32)
    target_sources(myapp PRIVATE regex_wrapper_pcre2.c)
    target_link_libraries(myapp PRIVATE pcre2-8)
endif()

或者你可以在一个源文件里用宏来切换:

// regex_wrapper.c
#include "regex_wrapper.h"

#if defined(__linux__) || defined(__APPLE__)
    #include "regex_wrapper_posix.c"
#elif defined(_WIN32)
    #include "regex_wrapper_pcre2.c"
#else
    #error "Unsupported platform"
#endif

我的小建议:我个人更推荐用CMake控制编译哪个源文件,而不是用#include .c文件。后者虽然省事,但会让IDE的代码分析工具发疯。

避坑指南:我踩过的几个雷

我曾经在封装PCRE2时,忘记处理pcre2_match_data的内存释放,结果服务跑了三天后内存暴涨。嗯,这种坑踩一次就记住了。

  • 内存管理:PCRE2的match_data每次匹配都要创建和释放,别复用同一个对象,除非你很清楚它的生命周期。
  • 错误码转换:POSIX的REG_NOMATCH是1,PCRE2的PCRE2_ERROR_NOMATCH是-1。封装时统一成0表示匹配成功,非0表示失败。
  • 线程安全:POSIX regex的regcomp不是线程安全的,如果你在多线程环境编译同一个正则,记得加锁。PCRE2在这方面就好很多。
  • 宽字符支持:如果你要处理Unicode,POSIX regex基本帮不上忙。PCRE2有pcre2_compile_81632三种变体,按需选择。

特别注意:POSIX regex的regcomp()第二个参数是const char *,但有些实现会修改它!我遇到过在某个老旧AIX系统上,pattern被截断的情况。保险起见,编译前先拷贝一份pattern。

性能对比:什么时候该用哪个?

你可能会问:既然PCRE2功能更强,为什么不直接全用PCRE2?原因很简单——依赖问题。在嵌入式Linux或者某些最小化系统上,PCRE2可能没装,但POSIX regex是标配。

我做过一个粗略的性能测试(匹配100万次简单邮箱正则):

编译时间 匹配时间 内存占用
POSIX regex 0.8 μs 1.2 μs ~2 KB
PCRE2 2.1 μs 0.9 μs ~8 KB

有意思吧?POSIX编译快,但匹配慢;PCRE2反过来。所以如果你要反复匹配同一个正则,PCRE2更划算。如果是一次性匹配,POSIX反而更快。

总结一下

跨平台正则封装,说白了就是抽象接口 + 条件编译 + 两套实现。别想着写一套代码兼容所有平台,那不现实。老老实实封装,上层代码干干净净,底层各玩各的,这才是工程化的做法。

我见过有些项目直接在代码里写#ifdef _WIN32然后调用std::regex,结果性能惨不忍睹。你想想看,std::regex在VC++上的实现,连基本的回溯都支持不全,何必自讨苦吃?

好了,这一章的内容就到这里。记住:封装不是目的,让上层代码不关心平台才是。


公众号:蓝海资料掘金营,微信 deep3321