ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CppNet 预处理器深度解析:Stride 中面向 Clang 的 C C/C++ 宏预处理器

CppNet 预处理器深度解析:Stride 中面向 Clang 的 C C/C++ 宏预处理器 游戏开发图形学VR【免费下载链接】strideStride (formerly Xenko), a free and open-source cross-platform C# game engine.项目地址https://gitcode.com/gh_mirrors/st/stride点击查看免费下载CppNet 是 Stride 引擎仓库中附带的一个 C 语言预处理器C Preprocessor实现它将 Java 世界的 JCPPAnarres C Preprocessor快速移植到 C#并针对 Clang 预处理场景做了增强。本文以 deps/CppNet/README.md 为核心脉络结合 Stride 仓库内实际 vendored 的源码与着色器管线中的真实调用系统讲解它的特性开关、宏系统、条件编译、include 解析、Clang 扩展指令__has_include/__has_feature等以及它在 Stride 的 SDSL 着色器预处理流程中的实际用途。读完本文你将能够独立配置一个 CppNetPreprocessor实例、理解其底层 token 流与状态栈工作机制并知道如何在 Stride 中复用它预处理 C/C/Objective-C 风格源码。CppNet 是什么从 JCPP 到 C# 的移植按照 deps/CppNet/README.md 的定义CppNet 是 JCPP 的quick and dirty快速粗糙C# 移植版其目标是支持 Clang 风格的预处理。JCPPAnarres C Preprocessor是 Shevek 维护的 Java 实现CppNet 在保留其整体架构的基础上额外添加了以下三个能力__has_include、__has_include_next、__has_feature三个 Clang 扩展指令可变参数宏variadic macros#import指令。在 Stride 仓库中CppNet 以两种形态存在预编译产物deps/CppNet/netstandard1.3/CppNet.dllnetstandard1.3 目标框架vendored 源码sources/shaders/Stride.Shaders.Parsers/CppNet/ 目录下完整保留了全部 C# 源文件其内部的 README.md 注明该副本来自 MonoGame/CppNet 上游仓库固定于 commit3dc3eb2db05be9d80b7cc5d00d5814cbe8f0ded2Make all public API internal for MonoGame Pipeline use. (#2)。也就是说Stride 拿到的这份副本把原先公开的 API 全部改成了internal仅供引擎内部管线使用而 deps/CppNet/README.md 中演示的仍是公开 API 形态的调用写法。同时该 vendored README 也列出了未被 Stride 使用的上游文件未随源码复制CppReader.cs、CppTask.cs、InputLexerSource.cs、TokenSnifferSource.cs、CppNet.csproj、CppNet.targets、Properties/AssemblyInfo.cs。许可证条款见 deps/CppNet/LICENSEApache License 2.0源码头部也保留了同样的版权声明。核心对象模型与整体架构从 Preprocessor.cs 的源码结构看CppNet 的架构与 JCPP 一脉相承核心由以下几部分组成PreprocessorPreprocessor.cs实现了IDisposable是整个预处理引擎。它维护inputs待处理的Source列表按添加顺序依次处理macrosDictionarystring, Macro宏表构造时即内置__LINE__、__FILE__、__COUNTER__三个特殊宏statesStackState条件编译状态栈#if/#else/#endif的嵌套靠它维护source当前正在读取的顶层Sourceinclude 时通过push_source/pop_source压栈/弹栈quoteincludepath、sysincludepath、frameworkspath分别对应-iquote、-I与 Objective-C frameworks 搜索路径features/warningsFeature与Warning两个位标志集合filesystem默认JavaFileSystem的VirtualFileSystem实例负责把路径解析为虚拟文件listenerPreprocessorListener事件监听器。Source族FileLexerSource.cs、StringLexerSource、LexerSource、FixedTokenSource、MacroTokenSource等统一提供token()接口是词法输入流的抽象。FileLexerSource提供三个构造重载(FileInfo)、(FileInfo, string path)、(string path)底层用StreamReader带缓冲读取。VirtualFile/VirtualFileSystem可插拔的虚拟文件系统setFileSystem允许完全替换路径解析逻辑。Token预处理输出是无需再次词法化的 token 流Token.getText()拼接即可还原文本。预处理器的入口方法有两个token()返回下一个预处理 tokentoken_nonwhite()跳过空白/注释后返回下一个有效 token。当没有安装监听器时源码中error()/warning()会直接抛出LexerException见 Preprocessor.cs。特性开关Feature 与 Warning 位标志CppNet 用两个[Flags]枚举精细控制行为均定义在独立文件中FeatureFeature.cs枚举值位含义NONE0无特性DIGRAPHS10支持 ANSI 双字符组digraphsTRIGRAPHS11支持 ANSI 三字符组trigraphsLINEMARKERS12输出行标记linemarkertokenCSYNTAX13将INVALID类型 token 报告为错误KEEPCOMMENTS14在词法输出中保留注释KEEPALLCOMMENTS15即使处于非激活分支也保留注释VERBOSE16冗余输出DEBUG17调试输出打印到 stderrOBJCSYNTAX18支持 Objective-C 词法如符号INCLUDENEXT19启用include_next指令Feature.DEBUG在 Preprocessor.cs 等多处控制Console.Error输出方便排查宏展开过程。WarningWarning.cs枚举值位含义NONE0无警告TRIGRAPHS10三字符组相关警告IMPORT11#import相关警告UNDEF12条件表达式中出现未定义标识符时告警UNUSED_MACROS13未使用的宏ENDIF_LABELS14#endif标签检查ERROR15把警告升级为错误特别注意Warning.ERROR开启后warning() 方法会直接转调error()让警告变成硬错误而Warning.UNDEF则会在#if表达式遇到未定义标识符时发出告警见 Preprocessor.cs。快速上手README 示例逐行解析deps/CppNet/README.md 给出了一个完整可运行的配置示例预处理一个 iOS ARM64 平台的 Objective-C 文件test.m逐行解读如下var pp new Preprocessor(); pp.addFeature(Feature.DIGRAPHS); pp.addFeature(Feature.TRIGRAPHS); pp.addFeature(Feature.OBJCSYNTAX); pp.addWarning(Warning.IMPORT); pp.addFeature(Feature.INCLUDENEXT); pp.setListener(new PreprocessorListener()); pp.getSystemIncludePath().Add(C:\XcodeDefault.xctoolchain\usr\include); pp.getSystemIncludePath().Add(C:\XcodeDefault.xctoolchain\usr\lib\clang\6.0\include); pp.getFrameworksPath().Add(C:\iPhoneOS8.0.sdk\System\Library\Frameworks); pp.getSystemIncludePath().Add(C:\iPhoneOS8.0.sdk\usr\include); pp.addMacro(__AARCH64_SIMD__); pp.addMacro(__ARM64_ARCH_8__); pp.addMacro(__ARM_NEON__); pp.addMacro(__LITTLE_ENDIAN__); pp.addMacro(__REGISTER_PREFIX__, ); pp.addMacro(__arm64, 1); pp.addMacro(__arm64__, 1); pp.addMacro(__APPLE_CC__, 6000); pp.addMacro(__APPLE__); pp.addMacro(__GNUC__, 4); pp.addMacro(OBJC_NEW_PROPERTIES); pp.addMacro(__STDC_HOSTED__, 1); pp.addMacro(__MACH__); Version version new Version(8.0.0.0); pp.addMacro(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__, string.Format({0:0}{1:00}{2:00}, version.Major, version.Minor, version.Revision)); pp.addMacro(__STATIC__); pp.addInput(new FileLexerSource(test.m));这段代码蕴含了 CppNet 的完整使用套路开关配置addFeature打开 digraphs/trigraphs、Objective-C 语法、include_nextaddWarning打开#import警告setListener挂接事件监听器不挂监听器时错误/警告会直接抛异常。搜索路径getSystemIncludePath()返回的Liststring可自由修改等价于编译器的-IgetFrameworksPath()对应 Objective-C 的 frameworks 搜索路径。include()的解析顺序见 Preprocessor.cs为带引号的 include 先查当前文件所在目录再查 quote 路径然后查系统 include 路径最后查 frameworks 路径。预定义宏addMacro(name)等价于addMacro(name, 1)源码注释明确说明addMacro(name, value)会把 value 用StringLexerSource词法化为 token 流作为宏展开体addMacro(__REGISTER_PREFIX__, )表示定义为空字符串。这里定义的__APPLE__、__arm64、__ARM_NEON__、OBJC_NEW_PROPERTIES等正是 Clang 在 Apple ARM64 目标上会自动定义的平台宏目的是让被预处理的头文件认为自己运行在真实 Clang 环境中。__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__则用Version(8.0.0.0)格式化出形如80000的部署目标版本号。输入源addInput(new FileLexerSource(test.m))把文件作为预处理入口。宏系统定义、展开、可变参数、字符串化与粘贴宏是预处理器的灵魂。CppNet 中addMacro有三种重载addMacro(Macro m)、addMacro(string name, string value)、addMacro(string name)。底层 addMacro(Macro) 会拒绝名为defined的宏抛出LexerException(Cannot redefine name defined)。宏体由Macro类封装Macro.csMacro持有名字、参数列表、variadic标志和展开 token 流isFunctionLike()判断是否为函数式宏参数列表非 nullgetArgs()返回参数个数addPaste/getText支持##粘贴操作符的字节码式表达。#define指令的处理在 define() 中完成支持函数式宏参数列表解析支持可变参数遇到...时调用m.setVariadic(true)并把__VA_ARGS__加入参数表且校验省略号必须在最后一个参数支持粘贴操作符##转换为M_PASTE内部 token支持字符串化操作符#当#后紧跟一个形参名时转换为M_STRINGtoken记录参数索引。展开过程在 macro() 中实现函数式宏会贪婪读取实参正确处理嵌套括号与空实参并校验参数个数匹配然后逐个args[i].expand(this)展开后压入MacroTokenSource。三个内置宏__LINE__、__FILE__、__COUNTER__走特殊路径Preprocessor.cs分别压入一个FixedTokenSource__COUNTER__每次展开自增counter。条件编译与表达式求值CppNet 完整支持#if、#ifdef、#ifndef、#elif、#else、#endif。其机制是每个#if/#ifdef/#ifndef都push_state()压入一个 StatePreprocessor.csisActive()需要父状态与自身同时激活#endif对应pop_state()且会检测没有#if的#endif。#if表达式求值由 expr() 完成是标准的递归下降表达式解析expr_priorityPreprocessor.cs给出了完整的 C 运算符优先级表——/、%、*11 级、-10 级、9 级、、、8 级、!7 级6 级^5 级|4 级3 级||2 级三元?:1 级外加一元~、!、-与括号支持整数、字符字面量除零/模零会报错还实现了三元表达式。defined(x)/defined x两种写法都支持expr_token()未定义标识符在条件表达式中默认按 0 处理只有开启Warning.UNDEF才告警。#ifdef/#ifndef直接查询宏表macros.ContainsKey(text)决定分支激活Preprocessor.cs。include / include_next / import 与虚拟文件系统include 解析是 CppNet 与 Clang 兼容性的关键支持#include ...与#include ...两种形式token 类型分别是STRING与HEADER解析顺序include(String, int, string, bool, bool, bool)带引号先查当前文件父目录 → quote include 路径 → 系统 include 路径 → frameworks 路径找不到时报File not found: name并列出所有已搜索路径frameworks 查找由 includeFramework() 实现把FrameworkName/Header.h拼成FrameworkName.framework/Headers/Header.h再逐路径搜索#include_next受Feature.INCLUDENEXT门控Preprocessor.cs未开启时报Directive include_next not enabled#import由 import() 转调 include并通过_importedPaths列表去重——同一个文件只导入一次Preprocessor.cs所有路径解析都经过VirtualFileSystem默认JavaFileSystem通过setFileSystem(VirtualFileSystem)可以整体替换为自定义实现如内存文件系统。预处理器的指令分发表ppcmdsPreprocessor.cs共登记 15 个指令define、elif、else、endif、error、if、ifdef、ifndef、include、line、pragma、undef、warning、include_next、import。未知指令会直接报错Unknown preprocessor directive。__has_include / __has_include_next / __has_feature面向 Clang 的扩展这是 CppNet 相对 JCPP 最重要的增强全部作用于#if条件表达式__has_include(x)/__has_include(x)由 has_include(false) 实现——它以checkOnlytrue模式走完整的 include 搜索逻辑但不实际压栈文件找到返回 1否则 0__has_include_next(...)同样的逻辑has_include(true)从下一个搜索路径开始__has_feature(feature)由 has_feature() 实现内部是一个巨大的 Clang 特性名switch覆盖objc_arc、objc_bool、blocks、cxx_lambdas、cxx_constexpr、cxx_rtti、cxx_variadic_templates、各类 sanitizeraddress_sanitizer/thread_sanitizer等以及 C11 特性c_alignas、c_atomic等上百个特性名命中返回 1未知特性返回 0。在表达式求值路径 expr() 中这三个标识符会被识别并按上述逻辑折叠成整型常量 0/1。此外在defined(...)判定里__has_include、__has_include_next、__has_feature也被视为已定义Preprocessor.cs兼容 SDK 头文件里#if defined(__has_include)的惯用写法。监听器与错误处理PreprocessorListenerPreprocessorListener.cs是一个三方法接口handleWarning(Source source, int line, int column, string msg)handleError(Source source, int line, int column, string msg)handleSourceChange(Source source, string ev)——source 压栈/弹栈时触发事件值为suspend、push、pop、resume见 push_source/pop_source。接口注释明确说明如果没有安装监听器所有错误与警告都会以异常形式抛出PreprocessorListener.cs安装监听器后才能实现记录日志继续运行这类更智能的处理。仓库还提供了DefaultPreprocessorListener作为默认实现。#error与#warning指令也走这套机制error(Token, bool)。行标记Linemarkers当开启Feature.LINEMARKERS时CppNet 会在 include 进入/返回处输出形如# linenum filename flags的行标记 tokenToken.P_LINE由 line_token() 生成EmitExtraLineInfo属性默认 true控制是否附加额外标志。源码注释Preprocessor.cs说明了四个标志的含义1进入新文件2从被包含文件返回3后续文本来自系统头文件应抑制部分警告4后续文本应被当作隐式extern C块。在 Stride 中的真实应用SDSL 着色器预处理CppNet 在 Stride 中不是孤立依赖而是服务于 SDSLStride 着色器语言的预处理环节。入口位于 MacroPreProcessor.cs其中静态类MonoGamePreProcessor提供了两个方法OpenAndRun(string filepath, params ReadOnlySpan(string Name, string Definition) defines)读取文件后转调RunRun(string content, string? filename, params ReadOnlySpan(string Name, string Definition) defines)核心逻辑。其关键调用序列与 README 示例高度一致var cpp new Preprocessor(); cpp.addFeature(Feature.DIGRAPHS); cpp.addWarning(Warning.IMPORT); cpp.addFeature(Feature.INCLUDENEXT); // 透传 defines foreach (var (Name, Definition) in defines) { if (!string.IsNullOrWhiteSpace(Name)) cpp.addMacro(Name, Definition ?? string.Empty); } var inputSource new StringLexerSource(content, true, filename!); cpp.addInput(inputSource); // 循环读取 token重建文本 while (!isEndOfStream) { Token tok cpp.token(); switch (tok.getType()) { case Token.EOF: isEndOfStream true; break; case Token.CCOMMENT: // 逐字符替换为空格保留换行 case Token.CPPCOMMENT: // 直接丢弃 default: textBuilder.Append(tok.getText()); } }这个封装展示了 CppNet 的标准消费方式以字符串为输入源StringLexerSource逐个取出 token把块注释替换为等长空格保证行列号不变、丢弃行注释、其余文本按getText()拼接最终得到预处理后的源码文本。这正是 Shader 编译器在进入语法分析前对.sdsl进行宏展开、条件编译与#include展开的实际路径。其底层对应的Token类型定义见 Token.cs词法入口见 LexerSource.cs。从仓库获取与集成方式直接引用预编译 DLL仓库已在 deps/CppNet/netstandard1.3/ 提供了 netstandard1.3 版本的CppNet.dll含 PDB适合外部项目直接引用deps/CppNet/checkout.bat是依赖获取脚本。使用 vendored 源码需要把 sources/shaders/Stride.Shaders.Parsers/CppNet/ 目录下的源文件加入编译单元。注意此副本为 MonoGame 管线做过 API 内部化类型均为internalStride 正是通过MonoGamePreProcessor间接使用若需对外暴露 API 请自行调整可见性。许可证遵循 Apache License 2.0见 deps/CppNet/LICENSE。CppNet 的完整指令集、宏展开语义、条件求值优先级与 Clang 扩展均已在此文中结合源码逐项核实如需深入阅读实现细节Preprocessor.cs 的 2200 余行代码是最权威的参考。赞分享游戏开发图形学VR【免费下载链接】strideStride (formerly Xenko), a free and open-source cross-platform C# game engine.项目地址https://gitcode.com/gh_mirrors/st/stride点击查看免费下载相关推荐C宏定义管理vscode-cpptools预处理器配置C宏定义管理vscode cpptools预处理器配置 引言宏定义管理的痛点与解决方案 你是否曾在大型C项目中遭遇过宏定义冲突导致的编译错误是否因开发工具调试器VSCode C/C扩展中预处理宏高亮问题的分析与解决VSCode C/C扩展中预处理宏高亮问题的分析与解决 在VSCode的C/C开发环境中预处理宏的高亮显示是一个重要的代码可视化功能。本文深入分析了该开发工具调试器SAM 自定义训练实战一条把 segment-anything 微调进业务数据的完整路线图SAM 自定义训练实战一条把 segment anything 微调进业务数据的完整路线图 通用 SAM 在街景和宠物照片上很好用换到你的医疗影像、工业缺陷人工智能计算机视觉基础模型上一篇如何使用Epinio从应用到URL的Kubernetes一键部署解决方案下一篇高性能Minecraft反向代理项目推荐创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表