Hello,ANTLR4
ANTLR 是一个用 Java 写的语法分析工具,类似 Lex Yacc 以及 Flex
Bison(这两个都有点太老了,而且Windows上也不好用),通过编写一个内嵌代码的文件(.g4
)来定义文法,然后由
ANTLR 对文件进行分析,生成不同后端的分析程序,例如 C++、Python、Java
等,相比我们手写分析程序,只要我们定义好文法,就可以完成解析过程,提高开发效率。
使用 ANTLR4 的最简单方式就是 Python 了,我们直接通过 pip 安装即可
1
| pip install antlr4-tools
|
由于 ANTLR 需要 Java ,如果没有安装 Java
环境会在第一次运行时自动安装。
之后我们可以定义一个简单的表达式文法来测试效果
Expr.g4
1 2 3 4 5 6 7 8 9
| grammar Expr; prog: expr EOF ; expr: expr ('*'|'/') expr | expr ('+'|'-') expr | INT | '(' expr ')' ; NEWLINE : [\r\n]+ -> skip; INT : [0-9]+ ;
|
(注意到文法中包含左递归,但是 ANTLR 会自动帮我们处理好)
通过下列命令测试文法
1
| antlr4-parse Expr.g4 prog -tree
|
其命令格式如下
1 2 3 4
| java org.antlr.v4.gui.Intrepreter [X.g4|XParser.g4 XLexer.g4] startRuleName [-tokens] [-tree] [-gui] [-encoding encodingname] [-trace] [-profile filename.csv] [input-filename(s)] Omitting input-filename makes rig read from stdin.
|
prog 对应于 startRuleName
此时会等待我们输入,我们输入一个有效的表达式
然后输入 Ctrl + D(Unix系统)或者
Ctrl
+Z(Windows系统)来终止输入,此时就可以看到输出结果
(由于是在 Python 中调用 Java 命令行,可以有点慢)
1 2 3
| 10*(4+2) ^Z (prog:1 (expr:1 (expr:3 10) * (expr:4 ( (expr:2 (expr:3 5) + (expr:3 2)) ))) <EOF>)
|
可以看到其成功对我们的文法进行分析,并通过嵌套关系表示出语法分析树,这样用只是展示
ANTLR 的分析效果,但是实际我们需要将其和应用集成起来,那么就需要使用
ANTLR
编译文法文件后生成不同语言的分析代码,从而实现高效的分析(最开始本来想用
ANTLR来进行 HTTP
报文的解析的,但是好像有点杀鸡用牛刀的意思,而且其无法支持流式输入,后面就自行写了一个简单的),不过对于
URL 解析或者 MySQL 语句的解析倒是挺好用的。
antlr4/doc/getting-started.md
at dev · antlr/antlr4 (github.com)
ANTLR4 for CPP
在 C++ 环境中使用 ANTLR4 相对而言较为繁琐,其包含两个步骤
- 编译文法文件,生成对应的 C++ 解析代码(
Lexer.cpp
以及
Parser.cpp
)
- 链接 ANTLR4 的 C++ 运行库
由于 C++
本身的特殊性,第一个步骤可以通过命令行手动完成,但是第二个编译过程就很麻烦了,不过官方也给出了样例代码:
antlr4/runtime/Cpp/README.md
at dev · antlr/antlr4 (github.com)
一般而言,只需要将 <ANTLR4_ROOT>/runtime/Cpp/cmake
文件夹中的 FindANTLR.cmake
以及
ExternalAntlr4Cpp.cmake
拷贝到项目本地,然后在
CMakeLists.txt
中 include
进来即可
示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // 将两个 .cmake 文件放在 cmake 文件夹下 list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
// 设置编译 ANTLR4 的一些选项,MSVC CRT链接方式(MT/MD)、静态/动态链接库等 // 设置所需要的 ANTLR4 版本(此处选择最新版) set(ANTLR4_TAG 4.13.0) // 添加外部依赖,通过 ExternalProject_Add 方式实现 // 其默认会创建 static 和 shared 两种库,可以自行选择需要的库进行链接 include(ExternalAntlr4Cpp)
// 对于第二个步骤,FindANTLR.cmake 提供 antlr_target 宏来自动化编译文法文件的过程,其使用示例如下 antlr_target( ExprParser Expr.g4 PARSER LEXER PACKAGE parser ) // 其会自动生成对应语言的分析代码,同时输出三个 cmake 变量,我们可以将其添加到编译目标中 add_executable(demo main.cpp) target_sources(demo PRIVATE ${ANTLR_ExprParser_CXX_OUTPUTS}) target_include_directories(demo PRIVATE ${ANTLR_ExprParser_OUTPUT_DIR})
// 最后连接到 antlr 的 cpp runtime 即可 target_link_libraries(demo PRIVATE antlr4_static)
|
个人在使用过程中感觉整个过程用起来有点别扭,主要有两个地方
- 使用
ExternalAntlr4Cpp
时并没有为我们创建 ALIAS
目标(即 glm::glm
这种,可以防止拼写错误问题),并且在编译时才会触发下载,无法提前代码状态(因为其使用的是
ExternalProject_Add
)
- 通过
antlr_target
方式添加的目标也仅在编译过程中才会创建,没编译前也是什么都没有,这样我们就不知道什么时候编译出错了,我们可能想将其作为源代码的一部分进行管理(因为其使用的是
add_custom_command
实现,在编译时才会执行)。
针对官方提供集成方案的两个缺点,我们参考官方集成方案进行一些修改,将
ExternalProject_Add
替换为 FetchContent
实现,同时通过 execute_process
方式替换
add_custom_command
,使得在每次重新配置时都会触发下载。
自行实现的 ANTLR4 集成方式
FetchContent 下载依赖
思路很简单,就是下载 + add_subdirectory
的老套路,不过
ANTLR4 比较坑的是动态链接库和静态链接库的处理上,
需要添加很多处理来保证编译正确(主要是 Windows 恶心人的
__declspec(dllexport)
以及
__declspec(dllimport)
),而在 ANTLR4 的原始 CMakeLists.txt
设置有点问题(主要是宏定义的可见范围设置有误)
<ANTLR4_ROOT>/runtime/Cpp/runtime/CMakeLists.txt(L120~132)
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (WIN32) set(static_lib_suffix "-static") if (TARGET antlr4_shared) target_compile_definitions(antlr4_shared PUBLIC ANTLR4CPP_EXPORTS) endif() if (TARGET antlr4_static) target_compile_definitions(antlr4_static PUBLIC ANTLR4CPP_STATIC) endif() if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(extra_share_compile_flags "-MP /wd4251") set(extra_static_compile_flags "-MP") endif() endif()
|
其中问题就出在
target_compile_definitions(antlr4_shared PUBLIC ANTLR4CPP_EXPORTS)
上。
这个宏的作用就是控制 Windows DLL导出/DLL导入的,代码位于
<ANTLR4_ROOT>/runtime/Cpp/runtime/src/antlr4-common.h(L54~62)
1 2 3 4 5 6 7 8 9
| #ifdef ANTLR4CPP_EXPORTS #define ANTLR4CPP_PUBLIC __declspec(dllexport) #else #ifdef ANTLR4CPP_STATIC #define ANTLR4CPP_PUBLIC #else #define ANTLR4CPP_PUBLIC __declspec(dllimport) #endif #endif
|
可以看到,当启用 ANTLR4CPP_EXPORTS
时就会开启
dllexport,而没启用且不是静态链接就是 dllimport
.
但设置为 PUBLIC 时会在所有的编译单元中都启用该宏,导致编译
main.cpp
的时候会提示LINK2019错误,也就是找不到符号(当然找不到,本来应该是
import 的变成了 export,怎么可能找得到符号呢)。
这个问题改起来也很简单,只需要将 PUBLIC
改成
PRIVATE
即可(可以向官方提 ISSUE),在
ExternalAntlr4Cpp.cmake
中给出了解决方案(不知道为什么不直接修改这个代码)
通过创建 IMPORTED
的 target 并将该 target
私有链接到原有的库,就可以避免 ANTLR4CPP_EXPORTS
传递到我们的业务代码中了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| add_library(antlr4_shared SHARED IMPORTED) add_dependencies(antlr4_shared antlr4_runtime-build_shared) set_target_properties( antlr4_shared PROPERTIES IMPORTED_LOCATION ${ANTLR4_RUNTIME_LIBRARIES}) target_include_directories(antlr4_shared INTERFACE ${ANTLR4_INCLUDE_DIRS} )
if(ANTLR4_SHARED_LIBRARIES) set_target_properties( antlr4_shared PROPERTIES IMPORTED_IMPLIB ${ANTLR4_SHARED_LIBRARIES} ) endif()
|
不过 CMake 在使用 IMPORTED target
时比较恶心,需要手动指定链接库的位置,这个也在
ExternalAntlr4Cpp.cmake
中写死了(感觉不太优雅,不过能跑就行吧)
了解官方的解决方案后,我们自行编写起来就简单很多了
add-antlr4.cmake
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| function(add_antlr4 TAG) set(ANTLR_OPTIONS SHARED WITH_STATIC_CRT WITH_LIBCXX DISABLE_WARNINGS) set(ANTLR_ONE_VALUE_ARGS "") set(ANTLR_MULTI_VALUE_ARGS "") cmake_parse_arguments(ANTLR4CPP "${ANTLR_OPTIONS}" "${ANTLR_ONE_VALUE_ARGS}" "${ANTLR_MULTI_VALUE_ARGS}" ${ARGN}) include(CMakePrintHelpers) FetchContent_Declare( antlr4_cpp GIT_REPOSITORY https://github.com/antlr/antlr4 GIT_TAG ${TAG} GIT_SHALLOW TRUE ) FetchContent_Populate(antlr4_cpp)
set(antlr4_cpp_SOURCE_DIR ${antlr4_cpp_SOURCE_DIR}/runtime/Cpp CACHE INTERNAL "" )
file(STRINGS ${antlr4_cpp_SOURCE_DIR}/VERSION ANTLR_VERSION) cmake_print_variables(ANTLR_VERSION) cmake_print_variables(ANTLR4CPP_WITH_STATIC_CRT) cmake_print_variables(ANTLR4CPP_WITH_LIBCXX) cmake_print_variables(ANTLR4CPP_DISABLE_WARNINGS) cmake_print_variables(ANTLR4CPP_SHARED)
set(WITH_DEMO FALSE CACHE INTERNAL "") set(WITH_STATIC_CRT ${ANTLR4CPP_WITH_STATIC_CRT} CACHE INTERNAL "") set(WITH_LIBCXX ${ANTLR4CPP_WITH_LIBCXX} CACHE INTERNAL "") set(DISABLE_WARNINGS ${ANTLR4CPP_DISABLE_WARNINGS} CACHE INTERNAL "") set(ANTLR4_INSTALL FALSE CACHE INTERNAL "") set(ANTLR_BUILD_CPP_TESTS FALSE CACHE INTERNAL "") set(ANTLR4_INCLUDE_DIRS ${antlr4_cpp_SOURCE_DIR}/runtime/src CACHE INTERNAL "") set(ANTLR4_OUTPUT_DIR ${antlr4_cpp_BINARY_DIR}/runtime CACHE INTERNAL "" )
if(ANTLR4CPP_SHARED) set(ANTLR_BUILD_SHARED TRUE CACHE INTERNAL "") set(ANTLR_BUILD_STATIC FALSE CACHE INTERNAL "") else() set(ANTLR_BUILD_SHARED FALSE CACHE INTERNAL "") set(ANTLR_BUILD_STATIC TRUE CACHE INTERNAL "") endif()
if(MSVC) set(ANTLR4_STATIC_LIBRARIES ${ANTLR4_OUTPUT_DIR}/antlr4-runtime-static.lib CACHE INTERNAL "") set(ANTLR4_SHARED_LIBRARIES ${ANTLR4_OUTPUT_DIR}/antlr4-runtime.lib CACHE INTERNAL "") set(ANTLR4_RUNTIME_LIBRARIES ${ANTLR4_OUTPUT_DIR}/antlr4-runtime.dll CACHE INTERNAL "") else() set(ANTLR4_STATIC_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.a CACHE INTERNAL "") if(MINGW) set(ANTLR4_SHARED_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll.a CACHE INTERNAL "") set(ANTLR4_RUNTIME_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll CACHE INTERNAL "") elseif(CYGWIN) set(ANTLR4_SHARED_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dll.a CACHE INTERNAL "") set(ANTLR4_RUNTIME_LIBRARIES ${ANTLR4_OUTPUT_DIR}/cygantlr4-runtime-${ANTLR_VERSION}.dll CACHE INTERNAL "") elseif(APPLE) set(ANTLR4_RUNTIME_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.dylib CACHE INTERNAL "") else() set(ANTLR4_RUNTIME_LIBRARIES ${ANTLR4_OUTPUT_DIR}/libantlr4-runtime.so CACHE INTERNAL "") endif() endif()
add_subdirectory( ${antlr4_cpp_SOURCE_DIR} ${antlr4_cpp_BINARY_DIR} )
if(ANTLR4CPP_SHARED) add_library(antlr4_runtime_shared SHARED IMPORTED) target_include_directories(antlr4_runtime_shared INTERFACE ${ANTLR4_INCLUDE_DIRS} ) set_target_properties(antlr4_runtime_shared PROPERTIES IMPORTED_LOCATION ${ANTLR4_RUNTIME_LIBRARIES} ) set_target_properties(antlr4_runtime_shared PROPERTIES IMPORTED_IMPLIB ${ANTLR4_SHARED_LIBRARIES} ) add_library(antlr4::antlr4_shared ALIAS antlr4_runtime_shared) else() target_include_directories(antlr4_static INTERFACE ${ANTLR4_INCLUDE_DIRS}) add_library(antlr4::antlr4_static ALIAS antlr4_static) endif()
endfunction(add_antlr4)
|
其中我们指定了一些参数
TAG
:Antlr 的版本(可以使用 git commit id 以及
tag)
SHARED
:是否使用动态链接库
WITH_STATIC_CRT
:静态 CRT(MSVC编译flag为
-MT
/ -MTd
),默认是动态 CRT (MSVC编译flag为
-MD
/ -MDd
,这样我们就需要安装
vcredist)
WITH_LIBCXX
:在 unix 下是否连接到
libcxx,相当于一种另外的 c++ 运行库(对应的还有 libstdc++)
DISABLE_WARNINGS
:很简单,是否开启警告(我们在编译
Windows 动态链接库时就发现 W4275)
编译器警告(等级
2)C4275 | Microsoft Learn
不是什么大问题,但是很多,因为 ANTLR4 Runtime 的 所有异常都继承自
std::exception
,但是 MSVC 会提示
std::exception
并不是 DLL导出的,可能存在问题。
对于第二个,我们的修改很少,就是调整 add_custom_command
为 execute_process
,这样在每次进行 cmake
配置时就会生成,便于 debug。
完整代码
add-antlr4-parser.cmake
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| function(add_antlr4_parser TARGET_NAME INPUT_FILE) set(ANTLR_OPTIONS LEXER PARSER LISTENER VISITOR) set(ANTLR_ONE_VALUE_ARGS NAMESPACE OUTPUT_DIRECTORY) set(ANTLR_MULTI_VALUE_ARGS GENERATE_FLAGS) cmake_parse_arguments(ANTLR_TARGET "${ANTLR_OPTIONS}" "${ANTLR_ONE_VALUE_ARGS}" "${ANTLR_MULTI_VALUE_ARGS}" ${ARGN}) include(CMakePrintHelpers) set(ANTLR_${TARGET_NAME}_INPUT ${INPUT_FILE})
cmake_path(GET INPUT_FILE STEM ANTLR_INPUT)
if(ANTLR_TARGET_OUTPUT_DIRECTORY) set(ANTLR_${TARGET_NAME}_OUTPUT_DIR ${ANTLR_TARGET_OUTPUT_DIRECTORY}) else() set(ANTLR_${TARGET_NAME}_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/antlr4cpp_generated_src/${ANTLR_INPUT}) endif()
unset(ANTLR_${TARGET_NAME}_CXX_OUTPUTS)
if((ANTLR_TARGET_LEXER AND NOT ANTLR_TARGET_PARSER) OR (ANTLR_TARGET_PARSER AND NOT ANTLR_TARGET_LEXER)) list(APPEND ANTLR_${TARGET_NAME}_CXX_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}.cpp) set(ANTLR_${TARGET_NAME}_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}.interp ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}.tokens) else() list(APPEND ANTLR_${TARGET_NAME}_CXX_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Lexer.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Lexer.cpp ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Parser.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Parser.cpp) list(APPEND ANTLR_${TARGET_NAME}_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Lexer.interp ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Lexer.tokens) endif()
if(ANTLR_TARGET_LISTENER) list(APPEND ANTLR_${TARGET_NAME}_CXX_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}BaseListener.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}BaseListener.cpp ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Listener.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Listener.cpp) list(APPEND ANTLR_TARGET_GENERATE_FLAGS -listener) endif()
if(ANTLR_TARGET_VISITOR) list(APPEND ANTLR_${TARGET_NAME}_CXX_OUTPUTS ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}BaseVisitor.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}BaseVisitor.cpp ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Visitor.h ${ANTLR_${TARGET_NAME}_OUTPUT_DIR}/${ANTLR_INPUT}Visitor.cpp) list(APPEND ANTLR_TARGET_GENERATE_FLAGS -visitor) endif()
if(ANTLR_TARGET_NAMESPACE) list(APPEND ANTLR_TARGET_GENERATE_FLAGS -package ${ANTLR_TARGET_NAMESPACE}) endif()
list(APPEND ANTLR_${TARGET_NAME}_OUTPUTS ${ANTLR_${TARGET_NAME}_CXX_OUTPUTS})
execute_process( COMMAND ${Java_JAVA_EXECUTABLE} -jar ${ANTLR_EXECUTABLE} ${INPUT_FILE} -o ${ANTLR_${TARGET_NAME}_OUTPUT_DIR} -no-listener -Dlanguage=Cpp ${ANTLR_TARGET_GENERATE_FLAGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) cmake_policy(SET CMP0140 NEW) return(PROPAGATE ANTLR_${TARGET_NAME}_OUTPUT_DIR ANTLR_${TARGET_NAME}_CXX_OUTPUTS ANTLR_${TARGET_NAME}_OUTPUTS ) endfunction(add_antlr4_parser)
|
由于我们在 configure
过程中进行生成,删除了几个不必要的配置,调用参数含义如下:
TARGET_NAME
: 指定文法名词,例如
ExprParser
INPUT_FILE
:对应的文法文件,例如
Expr.g4
LEXER
、PARSER
、LISTENER
、VISITOR
都是 BOOL 类型,表示是否开启对应的代码生成
lexer 对应词法分析器,parser 对应语法分析其,listener 和 visitor
是用来遍历语法树的两种方式(当然也可以自行实现语法树的遍历)
NAMESPACE
:生成的分析器所属命名空间,例如
parser
,对应的语法分析器就是
parser::ExprParser
OUTPUT_DIRECTORY
:生成的代码输出路径,例如我们可以将其输出到源代码文件夹
${CMAKE_CURRENT_SOURCE_DIR}
GENERATE_FLAGS
:一些其他的 antlr
文法生成参数,前面的足够使用了, 这个一般用不到
调用实例如下
1 2 3 4 5 6
| add_antlr4_parser( CalculatorParser Calculator.g4 LEXER PARSER NAMESPACE parser OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/parser )
|
杂项
目前网上除了 ANTLR4 的 github
的文档外,官方文档上信息很少,要想真正深入学习 ANTLR 的使用还是得看
ANTLR 作者写的那本书 ANTLR 4权威指南,不过网上的 pdf
版本还很多,也有中文版,基本上取代了文档的位置(连基本的 g4 文件 grammar
都是书上讲的清楚一点,唉!)