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_t、regex_compile()这种风格,这样上层代码完全不用关心底层是谁。
关键原则:上层代码只调用你封装的接口,永远不要直接调用regcomp()或pcre2_compile()。否则,跨平台就是个笑话。
我画了一张图,帮你理清这个封装层次:
动手写封装:一个最小可用的跨平台正则库
废话不多说,直接上代码。这是我封装的一个精简版,核心就三个函数:编译、匹配、释放。
// 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_compile、regex_match、regex_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_8、16、32三种变体,按需选择。
特别注意: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++上的实现,连基本的回溯都支持不全,何必自讨苦吃?
好了,这一章的内容就到这里。记住:封装不是目的,让上层代码不关心平台才是。