news 2026/9/27 7:28:51

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppNet 预处理器深度解析:Stride 中面向 Clang 的 C C/C++ 宏预处理器
  • 游戏开发
  • 图形学
  • VR

【免费下载链接】stride

Stride (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 世界的 JCPP(Anarres 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 风格的预处理。JCPP(Anarres C Preprocessor)是 Shevek 维护的 Java 实现,CppNet 在保留其整体架构的基础上,额外添加了以下三个能力:

  • __has_include、__has_include_next、__has_feature三个 Clang 扩展指令;
  • 可变参数宏(variadic macros);
  • #import指令。

在 Stride 仓库中,CppNet 以两种形态存在:

  • 预编译产物:deps/CppNet/netstandard1.3/CppNet.dll(netstandard1.3 目标框架);
  • vendored 源码:sources/shaders/Stride.Shaders.Parsers/CppNet/ 目录下完整保留了全部 C# 源文件,其内部的 README.md 注明该副本来自 MonoGame/CppNet 上游仓库,固定于 commit3dc3eb2db05be9d80b7cc5d00d5814cbe8f0ded2("Make 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/LICENSE(Apache License 2.0,源码头部也保留了同样的版权声明)。

核心对象模型与整体架构

从 Preprocessor.cs 的源码结构看,CppNet 的架构与 JCPP 一脉相承,核心由以下几部分组成:

  • Preprocessor(Preprocessor.cs):实现了IDisposable,是整个预处理引擎。它维护:
    • inputs:待处理的Source列表(按添加顺序依次处理);
    • macros:Dictionary<string, Macro>宏表,构造时即内置__LINE__、__FILE__、__COUNTER__三个特殊宏;
    • states:Stack<State>条件编译状态栈(#if/#else/#endif的嵌套靠它维护);
    • source:当前正在读取的顶层Source(include 时通过push_source/pop_source压栈/弹栈);
    • quoteincludepath、sysincludepath、frameworkspath:分别对应-iquote、-I与 Objective-C frameworks 搜索路径;
    • features/warnings:Feature与Warning两个位标志集合;
    • filesystem:默认JavaFileSystem的VirtualFileSystem实例,负责把路径解析为虚拟文件;
    • listener:PreprocessorListener事件监听器。
  • Source族:FileLexerSource.cs、StringLexerSource、LexerSource、FixedTokenSource、MacroTokenSource等,统一提供token()接口,是词法输入流的抽象。FileLexerSource提供三个构造重载:(FileInfo)、(FileInfo, string path)、(string path),底层用StreamReader带缓冲读取。
  • VirtualFile/VirtualFileSystem:可插拔的虚拟文件系统,setFileSystem允许完全替换路径解析逻辑。
  • Token:预处理输出是"无需再次词法化"的 token 流,Token.getText()拼接即可还原文本。

预处理器的入口方法有两个:token()返回下一个预处理 token,token_nonwhite()跳过空白/注释后返回下一个有效 token。当没有安装监听器时,源码中error()/warning()会直接抛出LexerException(见 Preprocessor.cs)。

特性开关:Feature 与 Warning 位标志

CppNet 用两个[Flags]枚举精细控制行为,均定义在独立文件中:

Feature(Feature.cs):

枚举值位含义
NONE0无特性
DIGRAPHS1<<0支持 ANSI 双字符组(digraphs)
TRIGRAPHS1<<1支持 ANSI 三字符组(trigraphs)
LINEMARKERS1<<2输出行标记(linemarker)token
CSYNTAX1<<3将INVALID类型 token 报告为错误
KEEPCOMMENTS1<<4在词法输出中保留注释
KEEPALLCOMMENTS1<<5即使处于非激活分支也保留注释
VERBOSE1<<6冗余输出
DEBUG1<<7调试输出(打印到 stderr)
OBJCSYNTAX1<<8支持 Objective-C 词法(如@符号)
INCLUDENEXT1<<9启用include_next指令

Feature.DEBUG在 Preprocessor.cs 等多处控制Console.Error输出,方便排查宏展开过程。

Warning(Warning.cs):

枚举值位含义
NONE0无警告
TRIGRAPHS1<<0三字符组相关警告
IMPORT1<<1#import相关警告
UNDEF1<<2条件表达式中出现未定义标识符时告警
UNUSED_MACROS1<<3未使用的宏
ENDIF_LABELS1<<4#endif标签检查
ERROR1<<5把警告升级为错误

特别注意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 的完整使用套路:

  1. 开关配置:addFeature打开 digraphs/trigraphs、Objective-C 语法、include_next;addWarning打开#import警告;setListener挂接事件监听器(不挂监听器时错误/警告会直接抛异常)。
  2. 搜索路径:getSystemIncludePath()返回的List<string>可自由修改,等价于编译器的-I;getFrameworksPath()对应 Objective-C 的 frameworks 搜索路径。include()的解析顺序(见 Preprocessor.cs)为:带引号的 include 先查当前文件所在目录,再查 quote 路径,然后查系统 include 路径,最后查 frameworks 路径。
  3. 预定义宏: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的部署目标版本号。
  4. 输入源: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.cs):Macro持有名字、参数列表、variadic标志和展开 token 流,isFunctionLike()判断是否为函数式宏(参数列表非 null),getArgs()返回参数个数,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()压入一个 State(Preprocessor.cs),isActive()需要父状态与自身同时激活;#endif对应pop_state(),且会检测"没有#if的#endif"。
  • #if表达式求值由 expr() 完成,是标准的递归下降表达式解析:expr_priority(Preprocessor.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)可以整体替换为自定义实现(如内存文件系统)。

预处理器的指令分发表ppcmds(Preprocessor.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) 实现——它以checkOnly=true模式走完整的 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、各类 sanitizer(address_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)的惯用写法。

监听器与错误处理

PreprocessorListener(PreprocessorListener.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的行标记 token(Token.P_LINE),由 line_token() 生成;EmitExtraLineInfo属性(默认 true)控制是否附加额外标志。源码注释(Preprocessor.cs)说明了四个标志的含义:

  • 1:进入新文件;
  • 2:从被包含文件返回;
  • 3:后续文本来自系统头文件,应抑制部分警告;
  • 4:后续文本应被当作隐式extern "C"块。

在 Stride 中的真实应用:SDSL 着色器预处理

CppNet 在 Stride 中不是孤立依赖,而是服务于 SDSL(Stride 着色器语言)的预处理环节。入口位于 MacroPreProcessor.cs,其中静态类MonoGamePreProcessor提供了两个方法:

  • OpenAndRun(string filepath, params ReadOnlySpan<(string Name, string Definition)> defines):读取文件后转调Run;
  • Run(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 内部化(类型均为internal),Stride 正是通过MonoGamePreProcessor间接使用,若需对外暴露 API 请自行调整可见性。
  • 许可证:遵循 Apache License 2.0,见 deps/CppNet/LICENSE。

CppNet 的完整指令集、宏展开语义、条件求值优先级与 Clang 扩展均已在此文中结合源码逐项核实;如需深入阅读实现细节,Preprocessor.cs 的 2200 余行代码是最权威的参考。

  • 游戏开发
  • 图形学
  • VR

【免费下载链接】stride

Stride (formerly Xenko), a free and open-source cross-platform C# game engine.

项目地址:https://gitcode.com/gh_mirrors/st/stride
点击查看免费下载

相关推荐

上一篇:如何使用Epinio:从应用到URL的Kubernetes一键部署解决方案
下一篇:高性能Minecraft反向代理项目推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 7:27:53

让大模型稳定输出 JSON:Schema 校验、失败重试与降级的三层防线

背景&#xff1a;崩掉程序的往往不是答案错&#xff0c;而是格式 我们那个桌面工具的主流程很朴素&#xff1a;用户在聊天窗口里发一句自然语言&#xff0c;程序把它交给模型&#xff0c;要求返回一段 JSON&#xff0c;说明"用户想干什么、涉及哪些参数"&#xff0c;…

作者头像 李华
网站建设 2026/9/27 7:26:02

第2篇 Prometheus 服务发现:动态目标管理实战

随着容器化与微服务架构的普及&#xff0c;被监控对象的形态发生了根本变化。过去以物理机、虚拟机为主体的固定目标&#xff0c;如今逐步被 Kubernetes 编排下的 Pod、Service 等实例所取代。这些实例的 IP 由集群动态分配&#xff0c;重启即变、扩缩容以秒计&#xff0c;生命…

作者头像 李华
网站建设 2026/9/27 7:25:28

智能轨道插座系统技术选型维度与供应商能力评估

在建筑配电柔性化升级过程中&#xff0c;智能轨道插座凭借取电点位可调、布线集成度高的技术特性&#xff0c;在家装、办公、商铺、厂房等场景的应用规模持续扩大。当前市场上产品技术水平参差不齐&#xff0c;部分产品存在长期运行接触不良、安全防护配置不达标、售后服务缺失…

作者头像 李华
网站建设 2026/9/27 7:13:41

Ajenti Core Push 推送服务解析:基于 Socket.IO 的实时消息广播架构

后端运维 【免费下载链接】ajenti Ajenti Core and stock plugins 项目地址&#xff1a; https://gitcode.com/gh_mirrors/aj/ajenti 点击查看 免费下载 Ajenti 的 aj.plugins.core.api.push 模块提供了一个向浏览器客户端推送实时消息的服务&#xff0c;是任务进度、系统事件…

作者头像 李华