预处理器tomullery(字符串化#include)
Preprocessor tomfoolery (stringifying a #include)
注意:这个问题与OpenCL本身无关…请查看最后一段,了解我的问题的简洁陈述。但为了提供一些背景:
我正在编写一些使用OpenCL的C++代码。我喜欢将OpenCL内核的源代码保存在它们自己的文件中,以便于编码和维护(而不是将源代码直接作为字符串常量嵌入到相关的C++代码中)。这不可避免地导致了一个问题,即一旦到了分发二进制文件的时候,如何将它们加载到OpenCL运行时中——理想情况下,OpenCL源代码包含在二进制文件中,这样二进制文件就不需要位于某个目录结构中的特定位置即可知道OpenCL源码在哪里。
我想把OpenCL文件作为字符串常量包含在某个地方,最好不使用额外的构建步骤或外部工具(为了跨编译器/跨平台的易用性……即,不使用xxd
等)。我以为我偶然发现了一种基于这个线程中第二个答案的技术,比如:
#define STRINGIFY(src) #src
inline const char* Kernels() {
static const char* kernels = STRINGIFY(
#include "kernels/util.cl"
#include "kernels/basic.cl"
);
return kernels;
}
请注意,如果可能的话,我宁愿不在我的OpenCL代码中嵌入STRINGIFY
宏(就像上面提到的SO问题中所做的那样)。现在,这在Clang/LLVM编译器上运行得很好,但GCC死得很可怕(出现了"调用宏STRINGIFY的未终止参数列表"和与.cl文件内容相关的各种语法"错误")。所以,很明显,这种精确的技术在编译器中是不可用的(我还没有尝试过MSVC,但我希望它也能在那里工作)。。。如何将其最小化,使其在编译器之间工作?
总之,我想要一种符合标准的技术,将文件的内容包含为C/C++字符串常量,而无需调用外部工具或用无关代码污染文件。想法?
EDIT:正如Potatoswatter所指出的,上述行为是未定义的,因此一种真正的跨编译器预处理器技术,不涉及触摸要字符串化的文件,可能是不可能的(第一个发现对大多数/所有编译器都有效的令人发指的黑客攻击的人就得到了答案)。对于好奇的人来说,我最终按照第二个回复中的建议做了。。。也就是说,我将STRINGIFY
宏直接添加到我包含的OpenCL文件中:
在somefile.cl
:中
STRINGIFY(
... // Lots of OpenCL code
)
在somefile.cpp
:中
#define STRINGIFY(src) #src
inline const char* Kernels() {
static const char* kernels =
#include "somefile.cl"
;
return kernels;
}
这适用于我尝试过的编译器(Clang和GCC也是如此,因为它在宏中没有预处理器指令),并且至少在我的上下文中不会造成太大的负担(即,它不会干扰语法高亮显示/编辑OpenCL文件)。像这样的预处理器方法的一个特点是,由于相邻的字符串被连接在一起,所以可以编写
inline const char* Kernels() {
static const char* kernels =
#include "utility_functions.cl"
#include "somefile.cl"
;
return kernels;
}
只要STRINGIFY宏在两个.cl
文件中,字符串就会连接起来,从而允许您对OpenCL代码进行模块化。
本标准最相关的部分是§16.3/10:
由最匹配的外部括号限定的预处理标记序列形成了类函数宏的参数列表。列表中的各个参数由逗号预处理标记分隔,但匹配的内圆括号之间的逗号预处理令牌不会分隔参数。如果(在参数替换之前)任何参数都不包含预处理标记,则行为是未定义的。如果参数列表中存在预处理标记序列,否则这些标记将充当预处理指令,则行为是未定义的。
提取关键点:
- 您需要将头文件括在一对括号内,这样宏就不会认为文件中的每个逗号字符都会引入另一个参数。这些括号也将被字符串化,但应该不难处理
- 将
#include
放在参数列表中完全是官方未定义的行为,因此这将是不可移植的。编译器官方不知道您是否希望生成的字符串为"#include "kernels/util.cl""
传统的技术是使用像bin2c这样的程序,通常是仓促编写的。另一种方法是使用GNU binutils:中的objcopy
$ objcopy -I binary extensions.cfg -O elf32-little -B i386 --rename-section .data=.rodata extensions.o
$ objdump -x extensions.o
extensions.o: file format elf32-i386
extensions.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .rodata 00000447 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
00000000 l d .rodata 00000000 .rodata
00000000 g .rodata 00000000 _binary_extensions_cfg_start
00000447 g .rodata 00000000 _binary_extensions_cfg_end
00000447 g *ABS* 00000000 _binary_extensions_cfg_size
-O和-B标志必须与编译的对象文件之一的objdump输出相匹配,以满足链接器的要求,而部分重命名只是为了通知运行时链接器此数据是只读的。注意符号,映射到起始地址、结束地址和数据大小。它们每个都算作地址,所以在C中,你可以将它们与以下内容一起使用:
extern const char _binary_extensions_cfg_start, _binary_extensions_cfg_end;
extern const char _binary_extensions_cfg_size;
for (const char *p=&_binary_extensions_cfg_start; p<&_binary_extensions_cfg_end; p++)
do_something(p);
memcpy(somewhere, &_binary_extensions_cfg_start, (intptr_t)&_binary_extensions_cfg_size);
我意识到这两个都不是你想要的预处理器,但它根本不是为了做到这一点而设计的。不过,我很想知道是否可以。
你不能那样做;我很惊讶它能在叮当声中工作。如果你想直接在二进制文件中包含文本文件,你有一些选择:
1:编译前运行的预处理程序,它将.cl文件转换为定义字符串的.cpp文件。
2:在编译的可执行文件中通过编译器特定的存储来存储字符串数据。这就是像Visual Studio这样的工具包含.rc、.ico和其他用于构建Windows应用程序的文件的方式。当然,如前所述,这些都是编译器特有的。
最安全的方法是选择1。
以下是我在xcode C:中的操作
const char oursource[60000];
const char * oursourceptr = oursource;
const char * * oursourceptrptr = & oursourceptr;
// in function "readfileintostring":
char *fylnm = "/Developer/projs/myproj/mykernel.cl";
long enby; short pathref;
FSRef dink; FSPathMakeRef( (const UInt8 *) &fylnm, &dink, NULL );
SInt16 forkRefNum; HFSUniStr255 dataForkName; FSGetDataForkName(&dataForkName);
FSOpenFork( &dink, dataForkName.length, dataForkName.unicode, fsRdPerm, (FSIORefNum *) &pathref );
enby = 100000; FSRead( pathref, &enby, (void *) oursourceptr );
// .. then later ..
program = clCreateProgramWithSource(context, 1, (const char **) oursourceptrptr, NULL, &err);
它不是预处理器voodoo,但它对我有效,我可以在.cl文件中看到突出显示的语法,我甚至可以将.cl复制到.c中,更改一个#define,它以xcode c运行。…