在编译时将 C/C++ 常量、枚举和结构体传递给 C#






4.81/5 (24投票s)
CppHeader2CS:一个编译时工具,用于将 C/C++ 头文件中的常量、枚举和结构体转换为 C#。
- 下载源代码 - 16 KB (MD5 哈希: 2b424e4deb7b2ee0e0034845f85ae58d)
- 下载可执行文件 - 10 KB (MD5 哈希: 15f6f4fa221e8b64cfba71d5d9877f0f)
- 在 GitHub 上查看最新代码
引言
这个命令行工具可以从 C/C++ 头文件中提取 C/C++ 常量、预定义宏、结构体和枚举,然后输出到 C# 文件。这个工具不是一个功能齐全的 C/C++ 到 C# 的转换器——它不转换函数到 C#。这个工具旨在将 C/C++ 文件中的头信息复制到 C# 文件中,以便在 C/C++ 项目和 C# 项目之间共享常量、预定义宏、结构体和枚举。这种转换通常通过 Visual Studio 的预构建命令行事件来完成。目标是为 C/C++ 和 C# 项目维护一套相同的常量和枚举。这些常量和枚举从给定的 C/C++ 文件中提取、转换,然后写入一个通常已添加到 C# 项目的生成的 C# 文件中。转换速度非常快,对总编译时间的影响不大。
由于 C# 不支持与 C/C++ 同等级别的预定义宏,程序会将 #define 值转换为 C# 中的常量,并将其放在默认类中。鉴于 C/C++ 中的 #define 值是无类型的,CppHeader2CS 会尝试找出最适合用于 C# 输出的常量类型。简而言之,它首先尝试解析为 int,然后是 float,接着是 bool,如果所有方法都失败,它就将其保存为 string。例如:#Define cat 5
会被翻译成 public const int cat = 5;
,而 #define cat 5.0
会被翻译成 public const float cat = 5;
。通过在 #define 后面的注释中附加 C2CS_TYPE
命令,也可以强制指定任何类型。例如, #Define cat 5 // C2CS_TYPE: float
会被翻译成 public const float cat = 5;
。
为了实现转换,程序使用正则表达式来收集信息,然后将数据转换为其 C# 等效项,并保存到三个目标区域之一。这三个 C# 目标区域是顶部区域、命名空间区域和类区域(参见图中的橙色框)。StringBuilder 用于收集转换后的三个区域。第一个“顶部区域”StringBuilder 写入 C# 文件的最顶部。这可以是类似 using system.IO;
的内容,并且可以通过在 C/C++ 源文件中使用特殊命令 //C2CS_Top_Write using system.IO;
来添加。第二个 StringBuilder 输出到命名空间区域,通常包含结构体和枚举。最后一个位置是默认类区域,主要存放常量。最后,这些内容合并到一个文档中,并输出到文件或控制台。下面您将看到一张带有橙色框的输出图片。每个橙色框代表一个 StringBuilder。
灵感
过去,我曾多次需要在 C/C++ 项目和 C# 项目之间传递常量、枚举或简单的结构体。在网上搜索,我找不到任何能做到我想要的事情的工具。其他解决方案都是完整的 C/C++ 到 C# 的转换,速度太慢,无法在每次编译时运行。我确实找到了一个不错的 T2 模板项目,它做了一些我想要的功能,但总体而言,T2 模板很难阅读,并且不支持我需要的功能。我决定采用正则表达式的方法。RegEx 有时会让人恼火且难以阅读,但对于某些问题它们效果很好。
使用代码
使用说明
- 将 CppHeader2CS.exe 文件复制到某个位置
- 打开 C# 项目的项目属性,然后导航到“生成事件”部分。
- 在“预构建事件命令行”中输入类似以下内容:
C:\[工具位置]\CppHeader2CS.exe "$(ProjectDir)MyInput.h" "$(ProjectDir)myCppSharedItems.cs"
根据您的需求,您需要调整上面提到的文件名和位置。Visual Studio 使用宏按钮来帮助完成此操作。
命令行用法
CppHeader2CS.exe input_file [output_file]
input_file - 这是 C/C++ 源头文件。它应该是一个用于 C/C++ 与 C# 共享的简单文件。
output_file - (可选) 这是 C# 输出文件的名称。如果未提供名称,它将使用输入文件名并加上 .cs 扩展名。
命令列表 (位于 C/C++ 源文件中的注释中)
此程序支持许多可以在源文件中输入的命令。命令通过像注释一样添加。例如:// C2CS_TYPE MyType
这些命令可用于写入或调整输出。三个 C2CS_*_Write 命令可用于使用 C/C++ 源文件中的注释在 C# 文件中写入任何内容。其他选项可用于设置自定义命名空间或类名,以及强制指定类型或完全跳过源文件中的一行。
// C2CS_TOP_Write 要写入的文本 - C2CS_TOP_Write 是一个直接传递,将文本写入命名空间之上。当需要将内容打印在输出文件的顶部时,这非常有用。例如://C2CS_TOP_Write using System.IO;
// C2CS_NS_Write 要写入的文本 - C2CS_NS_Write 是一个直接传递,将文本写入默认类之上,但在命名空间区域。此区域通常包含枚举和结构体。
// C2CS_Class_Write 要写入的文本 - C2CS_Class_Write 是一个直接传递,将文本写入类区域。CppHeader2CS 仅写入一个类。
// C2CS_Set_Namespace MyNsName - C2CS_Set_Namespace 设置命名空间名称。这是可选的,如果未指定,则默认为 C2CS。
// C2CS_Set_ClassName MyClass - C2CS_Set_ClassName 设置要使用的类名。这是可选的,如果未指定,则默认为 Constants。
// C2CS_TYPE MyType - C2CS_TYPE 用于 #define 后面的注释中,以指定要使用的类型。如果自动检测不符合预期或程序员想强制指定类型,则需要此项。例如:#Default mySum (2+1) //C2CS_TYPE int;
// C2CS_SKIP - 在任何注释中添加 C2CS_SKIP 都会强制 CppHeader2CS 忽略当前行。
示例
示例 1 - #define -> int
#DEFINE Cars 10
...将添加到类区域,如...
public const int Cars = 10;
关注点
- 自动识别为 int。
示例 2 - #define -> float
#DEFINE MyDouble 3.2
...将添加到类区域,如...
public const double MyDouble = 3.2f;
关注点
#DEFINE
始终会转换为小写。#DEFINE MyFloat 3.2f
带“f”后缀将导致输出为 float。#DEFINE MyFloat 3.2d
带“d”后缀将导致输出为 double。- 当没有后缀时,输出将是 double。
- 科学计数法得到支持。
示例 3 - #define -> 支持非常简单的表达式
假设 my_float 是一个已存在的 float...
#DEFINE expr1 my_float + 2
...将添加到类区域,如...
public const float expr1 = my_float + 2;
关注点
- 转换器识别出这必须是一个 float。但它的功能非常有限,仅支持 +、-、/、* 以及逻辑 true、false、&&、||。
- 如果未按预期自动检测,则始终可以使用命令
// C2CS_TYPE MyType
。
示例 4 - #define -> string
#DEFINE MyString New World Test!
...将添加到类区域,如...
public const string MyString = "New World Test!";
关注点
- 如果
int.TryParse
、float.TryParse
和bool.TryParse
都失败,则使用 string 类型。
示例 5 - 简单的 #define,没有值
#DEFINE FullDebug
稍作修改并移至顶部...
#define FullDebug
关注点
- 未显示,但
#define
已移至 C# 文件顶部附近(C# 要求)。 - 大小写已更改为小写。
示例 6 - Struct
// C2CS_NS_Write [StructLayout(LayoutKind.Sequential)]
struct SomeStruct1{unsigned char a; unsigned char b;};
...添加到命名空间区域,如...
[StructLayout(LayoutKind.Sequential)]
public struct SomeStruct1
{
public byte a;
public byte b;
}
关注点
- 此处我们添加了前缀
// C2CS_NS_Write [StructLayout(LayoutKind.Sequential)]
。这将使结构体布局更像 C/C++ 结构体。仅在需要时添加。 - 单行结构体得到支持,但会转换为标准格式。
- 此处仅支持基本项,如 int、short、long、long long、char、char*、unsigned、float 和 double。
示例 7 - Struct #2
struct SomeStruct1{
unsigned long long test123; //some comments
public long test124;
} someStruct1Instance; // my instance notes
...添加到命名空间区域,如...
public struct SomeStruct1
{
public ulong test123; //some comments
public long test124;
}
...并且以下内容也添加到类部分...
public SomeStruct1 someStruct1Instance; // my instance notes
关注点
- “//”样式的注释会被保留,如果它们在同一行。
- 位字段目前不支持。
示例 8 - Enum
public enum MyEnum {
test1 = 33,
test2 = 5,
test3 = test2 };
...添加到命名空间区域,如...
[Flags]
enum MyEnum
{
test1 = 33,
test2 = 5,
test3 = test2,
};
关注点
- 仅当有赋值时,才会添加
[Flags]
前缀。这样做是为了保持枚举的含义不变。这将来可能会更改。
示例 9 - 常量/静态
protected static char myChar = -100; // my notes
...将添加到类区域,如...
public const SByte myChar = -100; // my notes
关注点
- 输出始终为
public const...
。 - protected 和 static 被忽略(private 和 const 也被忽略)。
- 值写入类部分。
性能
性能是此项目的重要组成部分。由于此工具通常会在每次编译时调用,因此保持其速度很重要。我使用了一些 RegEx 技巧,StringBuilders
,一个快速的空格移除器,以及一个 Dictionary
来提供帮助。转换相对较快。对于 224 行的演示文件,整个读取、转换和写入过程大约需要 12 到 30 毫秒,具体取决于使用的系统。较短的文件,约 40 行,大约需要 5 毫秒。
我在网上的一些“RegEx 技巧”中学到的一件事是,要确保没有发生灾难性回溯。简而言之,最好在 RegEx 表达式中更具体,并避免使用通配符“.*”。我没有遇到重大问题,但确实发现更具体的表达式可以将演示文件的性能提高约 50%。我还实现了一些其他技巧,这些技巧很容易在网上找到。然而,在构建过程中,我有几次机会可以稍微提高性能,但我为了更易读的代码而放弃了它们。
该文件也只有 37kb,这应该有助于提高其性能。39 个文件可以装在一张 1986 年的 1.44MB 软盘上。=)
限制
- 仅适用于预处理指令、常量、结构体和枚举。
- 不转换任何函数(它不是一个完整的 C/C++ 到 C# 的转换器)。
- 任何 /* ... */ 风格的注释都不会被带过来。在程序的第一个传递中,它们被简单地删除。它们可以在源文件的任何位置使用,但请注意,它们永远不会出现在输出文件中。
typedef
不受支持。bitfields
部分支持。- char 数组不受支持。
未来愿望清单
- 添加对简单函数转换的支持,例如:
int add(int a, int b){ return (a+b);}
- 添加
typedef
、char 数组和额外的位字段支持。
如果您想做出贡献,请随时在评论中发布更改或在 GitHub 上提交拉取请求。要发布到 GitHub:(1) 点击 Fork 按钮将存储库 fork 到您的帐户。(2) 进行更改并将您的更改推送到您自己的分支。(3) 创建一个拉取请求,将这些更改包含在原始存储库中。
关注点
能够如此轻松快速地识别和解析 C/C++ 文件是对正则表达式的绝佳证明。虽然一个非 RegEx 的 C/C++/C# 解析器可能具有更好的性能和控制力,但 RegEx 解决方案更容易、更快地投入使用。当程序员需要快速可靠地完成任何模式匹配时,RegEx 是一个强大的工具。没有 RegEx,代码将花费更长的时间来构建,而且我可能在完成项目之前就失去了兴趣!=)
其他值得尝试的工具
P/Invoke Interop Assistant - 这是引起我注意的一个工具,它做了与 cppHeader2CS 类似的事情。P/Invoke Interop Assistant 是一个工具,可以从 c++ 文件生成 C# 或 VB P/Invoke 代码,但它也生成结构体、枚举、#defines 和其他项。根据您的需求,此工具可能更适合。以下是两者的优点:
P/Invoke Interop Assistant 相对于 cppHeader2CS 的优点:
windows.h
类型- p/invoke 头文件生成(该工具的主要目的)
- 它支持 char 数组、typedef 和 include 文件。
- 输出到 VB。
- 总的来说,输出更适合 p/invoke 用途。
- 可能还有我遗漏的其他方面(它是一个更强大/更复杂的工具)。
总结:如果需要上述任何一项,请使用 Interop Assistant。此工具可以在构建时运行,但速度较慢,并且会始终更新目标文件。更新目标文件可能会很烦人,尤其是在文件打开的情况下,因为 Visual Studio 会提示您重新加载它。
cppHeader2CS 的优点:
- 支持更多预定义宏,IA 仅支持 #define。
- 支持全局常量。
- 支持自定义命名空间和类名。
- 允许通过注释中的命令进行引导式输出。
#define
-> const 的类型自动检测效果更好。- 速度更快,大约 0.1 秒 vs 0.7-3.0 秒(在我的系统上)。
- 如果没有更改,则不会覆盖输出。
- 小巧的 36kb 文件,无需安装。
总结:对于经常修改且在构建过程中更新的更简单的头文件,请使用 cppHeader2CS。此工具不支持 char 数组、typedef、自定义类型和 include 文件。
如果您想尝试使用此工具替代 cppHeader2CS 的说明:
- 访问 http://clrinterop.codeplex.com/releases/view/14120[^] 并下载程序。
- 安装程序。
- 在安装文件夹中“类似 C:\Program Files (x86)\InteropSignatureToolkit”的位置,有一个名为 sigimp.exe 的文件,这个文件是需要的。在“预构建事件命令行”区域使用类似 `"C:\Program Files (x86)\InteropSignatureToolkit\sigimp.exe" /genPreProc:yes /lang:cs /out:"$(ProjectDir)MyOutput.cs" "$(ProjectDir)MyInput.h"` 的内容。
使用 T4 模板 - 这是执行构建时 C/C++ 到 C# 转换的另一种方法。它只是一个简单的 T4 文件。下面链接中的示例仅处理 #define 到常量转换,并且可以自动检测简单的整数。我包含它是因为这是我在 cppHeader2CS 之前使用的工具。链接:http://stackoverflow.com/a/5638890
历史
- 2014/5/30 - 第一个工作程序,仅支持 #define。
- 2014/6/20 - 添加了更多预处理支持、结构体和常量支持。
- 2014/6/27 - 添加了对源文件命令的支持。
- 2014/7/12 - 添加了 #define 自动检测类型支持。
- 2014/7/21 - 添加了枚举支持。
- 2014/7/26 - 从 C2CS 重命名为 CppHeader2CS(C2CS 可能被误解为完整的 C 到 C# 转换器)。
- 2014/7/27 - 在 CodeProject 上首次发布。
- 10/15/2014
- 同文件自定义类型不受支持。 “struct A { ... }; struct B { A a;...}” 会被转换。
- 没有后缀的小数现在默认为 double。
- 添加了“d”后缀支持。
- 添加了额外的 double 支持。
- 添加了 void* 支持。
- 位字段结构体被接受,但位部分被移除。
- 其他修复和代码清理。
- 5/6/2016
- 添加了接受 Linux/Mac OS X 风格的行尾 (\n) 行结束符的支持。
- 已添加到 GitHub。
- 5/22/2016
- 添加了对“if defined(...)”风格预定义宏的支持。
- 性能:添加了更快的移除空格的方法。
- 其他:一般性代码/注释清理(次要)。