使用 CMake 将资源(例如,着色器代码;图像)嵌入到可执行文件/库中

     2023-03-15     32

关键词:

【中文标题】使用 CMake 将资源(例如,着色器代码;图像)嵌入到可执行文件/库中【英文标题】:Embed resources (eg, shader code; images) into executable/library with CMake 【发布时间】:2012-08-02 12:54:50 【问题描述】:

我正在用 C++ 编写一个应用程序,它依赖于我项目中的各种资源。现在,我拥有从生成的可执行文件到源中硬编码的每个资源的相对路径,这允许我的程序打开文件并读取每个资源中的数据。这工作正常,但它要求我从相对于资源的特定路径启动可执行文件。因此,如果我尝试从其他任何地方启动我的可执行文件,它将无法打开文件并且无法继续。

是否有一种可移植方法让 CMake 将我的资源嵌入到可执行文件(或库)中,以便我可以在运行时简单地在内存中访问它们,而不是打开路径脆弱的文件?我找到了一个related question,看起来嵌入资源可以通过一些ld 魔法来完成。所以我的问题是如何使用 CMake 以可移植、跨平台的方式做到这一点?我实际上需要我的应用程序在 x86 和 ARM 上运行。我可以只支持 Linux(嵌入式),但如果有人能建议如何为 Windows(嵌入式)执行此操作,则可以加分。

编辑: 我忘了提及解决方案的所需属性。当我为 ARM 构建时,我希望能够使用 CMake 来交叉编译应用程序,而不是必须在我的 ARM 目标上本地编译它。

【问题讨论】:

见github.com/graphitemaster/incbin 【参考方案1】:

执行此操作的最简单方法之一是在您的构建中包含一个小型、可移植的 C 程序,该程序读取资源并生成一个 C 文件,其中包含资源数据的长度和作为常量数组的实际资源数据字符文字。这将完全独立于平台,但只能用于相当小的资源。对于更大的资源,您可能不想将文件嵌入到程序中。

对于资源“foo”,生成的 C 文件“foo.c”将包含:

const char foo[] =  /* bytes of resource foo */ ;
const size_t foo_len = sizeof(foo);

要从 C++ 访问资源,请在使用它们的标头或 cpp 文件中声明以下两个符号:

extern "C" const char foo[];
extern "C" const size_t foo_len;

要在构建中生成foo.c,你需要一个C程序的目标(称为embedfile.c),你需要使用ADD_CUSTOM_COMMAND命令来调用这个程序:

add_executable(embedfile embedfile.c)

add_custom_command(
  OUTPUT foo.c
  COMMAND embedfile foo foo.rsrc
  DEPENDS foo.rsrc)

然后,在需要“foo”资源的目标的源列表中包含foo.c。您现在可以访问“foo”的字节了。

程序embedfile.c是:

#include <stdlib.h>
#include <stdio.h>

FILE* open_or_exit(const char* fname, const char* mode)

  FILE* f = fopen(fname, mode);
  if (f == NULL) 
    perror(fname);
    exit(EXIT_FAILURE);
  
  return f;


int main(int argc, char** argv)

  if (argc < 3) 
    fprintf(stderr, "USAGE: %s sym rsrc\n\n"
        "  Creates sym.c from the contents of rsrc\n",
        argv[0]);
    return EXIT_FAILURE;
  

  const char* sym = argv[1];
  FILE* in = open_or_exit(argv[2], "r");

  char symfile[256];
  snprintf(symfile, sizeof(symfile), "%s.c", sym);

  FILE* out = open_or_exit(symfile,"w");
  fprintf(out, "#include <stdlib.h>\n");
  fprintf(out, "const char %s[] = \n", sym);

  unsigned char buf[256];
  size_t nread = 0;
  size_t linecount = 0;
  do 
    nread = fread(buf, 1, sizeof(buf), in);
    size_t i;
    for (i=0; i < nread; i++) 
      fprintf(out, "0x%02x, ", buf[i]);
      if (++linecount == 10)  fprintf(out, "\n"); linecount = 0; 
    
   while (nread > 0);
  if (linecount > 0) fprintf(out, "\n");
  fprintf(out, ";\n");
  fprintf(out, "const size_t %s_len = sizeof(%s);\n\n",sym,sym);

  fclose(in);
  fclose(out);

  return EXIT_SUCCESS;

【讨论】:

这看起来是一个非常优雅、独立于平台的解决方案,+1。我会试试看。但是,对于我最初忘记提及的事情,有一个小问题(请参阅我的编辑)。如果我将此解决方案合并到项目中并尝试交叉编译它,我会在尝试执行embedfile 时遇到麻烦,因为该可执行文件将针对 ARM 进行编译,我会尝试在 x86 上执行它。我想我可以为我的主机平台单独编译embedfile 并将其安装到我的路径中。有点不方便,但我只需要做一次。 @SchighSchagh 可能有 CMake 标志,但我目前对交叉编译没有太多经验。如果您知道解释器在感兴趣的主机平台上可用,则 embedfile 很容易用 Python 或 Perl 等语言重写。 @SchighSchagh:你可能想看看这个:cmake.org/Wiki/… 我最终将您的 embedfile.c 移植到 Python,到目前为止,我对这个解决方案非常满意。 :) 我这里有一个 BSD python 版本:gist.github.com/jlisee/5667e173bd2865a68f95 它可以从 stdin/stdout 输出读/写,允许您单独设置输出路径和符号名称,并具有嵌入式测试。【参考方案2】:

我想说在 C++ 中嵌入资源的最优雅的方法就是使用 Qt 资源系统,它可以跨不同平台移植,与 CMake 兼容,并且除了提供压缩之外,基本上包含了上面答案中所做的所有事情,经过全面测试和万无一失,其他一切。

创建一个 Qt 资源文件 - 一个列出要嵌入的文件的 XML:

<RCC>
    <qresource prefix="/">
        <file>uptriangle.png</file>
        <file>downtriangle.png</file>
    </qresource>
</RCC>

调用文件 qtres.qrc。上面的资源文件将在最终的可执行文件中嵌入两个 png 文件(与 qtres.qrc 位于同一目录中)。您可以使用 QtCreator(Qt IDE)轻松地将监视器资源添加/删除到 qrc 文件。

现在在你的 CMakeLists.txt 文件中添加:

set(CMAKE_AUTOMOC ON)
find_package(Qt5Core)
qt5_add_resources(QT_RESOURCE qtres.qrc)

在您的 main.cpp 中,在您需要访问资源之前,添加以下行:

Q_INIT_RESOURCE(qtres);

现在,您可以使用与 Qt 资源系统兼容的 Qt 类访问上述任何资源,例如 QPixmap、QImage ... 最重要的是,在一般情况下,可能是包装嵌入式 Qt 资源并允许访问的 QResource 包装器类它通过一个友好的界面。例如,要访问上述资源中 downtriangle.png 中的数据,以下几行就可以了:

#include <QtCore>
#include <QtGui>

// ...

int main(int argc, char **argv)


    // ...

    Q_INIT_RESOURCE(qtres);

    // ...
    QResource res("://downtriangle.png"); // Here's your data, anyway you like
    // OR
    QPixmap pm("://downtriangle.png");  // Use it with Qt classes already

    // ...


这里可以通过 res.data(), res.size() ... 直接访问数据 要解析文件的图像内容,请使用 pm。使用 pm.size(), pm.width() ...

你可以走了。 我希望它有所帮助。

【讨论】:

Qt 对我来说是一个非常严重的依赖项,但无论如何 +1 以获得一个好的答案。 我目前正在为 ARM/DSP 平台开发一个嵌入式应用程序,它专门使用 STL + Boost 并设法将 QtResources 与它一起使用,这基本上总结了第一个答案中所做的事情引擎盖。除了 QResource (如果您将文件用作原始二进制资源)和 Qt5Core cmake 模块(框架附带,也可以独立使用默认发行版配置)之外,没有依赖项(至少不是重的)。看一下qt5_add_resources(QT_RESOURCE qtres.qrc)命令生成的资源文件。 Qt 资源系统很棒,但此答案仅适用于您想要并被允许使用 Qt 的情况。例如,slim SDK lib 文件不太可能使用此方法。【参考方案3】:

作为sfstewman 答案的替代方案,这里有一个小的 cmake (2.8) 函数,用于将特定文件夹中的所有文件转换为 C 数据并将它们写入希望的输出文件中:

# Creates C resources file from files in given directory
function(create_resources dir output)
    # Create empty output file
    file(WRITE $output "")
    # Collect input files
    file(GLOB bins $dir/*)
    # Iterate through input files
    foreach(bin $bins)
        # Get short filename
        string(REGEX MATCH "([^/]+)$" filename $bin)
        # Replace filename spaces & extension separator for C compatibility
        string(REGEX REPLACE "\\.| |-" "_" filename $filename)
        # Read hex data from file
        file(READ $bin filedata HEX)
        # Convert hex data for C compatibility
        string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," filedata $filedata)
        # Append data to output file
        file(APPEND $output "const unsigned char $filename[] = $filedata;\nconst unsigned $filename_size = sizeof($filename);\n")
    endforeach()
endfunction()

【讨论】:

string(MAKE_C_IDENTIFIER $filename filename) 可以替换 string(REGEX REPLACE...) 我发现 MAKE_C_IDENTIFIER 不适用于文件名中的句点和连字符。原来的正则表达式也不起作用。我对其进行了一些修改,以便创建的变量在这些情况下也使用下划线:string(REGEX REPLACE "\\.| |-" "_" filename $filename) 另外,另一个方便的补充是,如果将数据的输出更改为file(APPEND $output "const char $filename[] = $filedata0x00;\nconst unsigned $filename_size = sizeof($filename);\n") ,则可以将文件的内容直接输出到类似 cout 的流中,而无需转换或强制转换。 0x00 也会自动终止它,因此在以这种方式使用时,您不会开始打印内存的随机值。 它非常适合我,谢谢。我只想知道是否有办法在修改资源文件而不显式调用 CMake 时重新运行此函数,以避免忘记重新运行它,例如在 git bisect 期间 @Jepessen 这应该是一个自定义命令而不是一个函数。【参考方案4】:

我想提出另一种选择。它使用 GCC 链接器将二进制文件直接嵌入到可执行文件中,无需中间源文件。在我看来,这更简单、更高效。

set( RC_DEPENDS "" )

function( add_resource input )
    string( MAKE_C_IDENTIFIER $input input_identifier )
    set( output "$CMAKE_ARCHIVE_OUTPUT_DIRECTORY/$input_identifier.o" )
    target_link_libraries( $PROJECT_NAME $output )

    add_custom_command(
        OUTPUT $output
        COMMAND $CMAKE_LINKER --relocatable --format binary --output $output $input
        DEPENDS $input
    )

    set( RC_DEPENDS $RC_DEPENDS $output PARENT_SCOPE )
endfunction()

# Resource file list
add_resource( "src/html/index.html" )

add_custom_target( rc ALL DEPENDS $RC_DEPENDS )

那么在你的 C/C++ 文件中你只需要:

extern char index_html_start[] asm( "_binary_src_html_index_html_start" );
extern char index_html_end[]   asm( "_binary_src_html_index_html_end" );
extern size_t index_html_size  asm( "_binary_src_html_index_html_size" );

【讨论】:

我想这就是我最初想要的! MAKE_C_IDENTIFIER 是如何工作的/它来自哪里? 另外,你知道asm 的东西是否适用于非x86 和/或交叉编译? 其实asm真的需要吗?我在猜测它是如何工作的,但你能不能只做extern char _binary_src_html_index_html_start[]; 等,然后就这样吗?然后,是否可以使用“原始”标识符或以某种方式为其别名,例如通过额外的const 变量,或通过宏,或其他方式,这是一个品味问题。 @NicuStiurca 我很感激这一点,因为我迟到了 7 年。 1. MAKE_C_IDENTIFIER 是一个 CMake 语言函数,可从字符串生成与 C 兼容的变量名称(下划线)。这是一种从输入文件路径生成文件系统友好和描述性文件名的简短而简洁的方法。 2. asm 是特定于编译器的,但大多数都支持它,包括 GCC、Intel、IBM 等。有条件地支持它,但据我所知,它在大多数情况下应该可以工作。 3. 你是对的。这只是重命名目标文件的长名称以使您的代码更具可读性的一种方便方法。【参考方案5】:

纯 CMake 函数将任何文件转换为 C/C++ 源代码,仅使用 CMake 命令实现:

####################################################################################################
# This function converts any file into C/C++ source code.
# Example:
# - input file: data.dat
# - output file: data.h
# - variable name declared in output file: DATA
# - data length: sizeof(DATA)
# embed_resource("data.dat" "data.h" "DATA")
####################################################################################################

function(embed_resource resource_file_name source_file_name variable_name)

    file(READ $resource_file_name hex_content HEX)

    string(REPEAT "[0-9a-f]" 32 column_pattern)
    string(REGEX REPLACE "($column_pattern)" "\\1\n" content "$hex_content")

    string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " content "$content")

    string(REGEX REPLACE ", $" "" content "$content")

    set(array_definition "static const unsigned char $variable_name[] =\n\n$content\n;")

    set(source "// Auto generated file.\n$array_definition\n")

    file(WRITE "$source_file_name" "$source")

endfunction()

https://gist.github.com/amir-saniyan/de99cee82fa9d8d615bb69f3f53b6004

【讨论】:

【参考方案6】:

有一个名为 cmrc 的单文件 CMake 脚本可让您轻松嵌入数据。

示例用法:

include(CMakeRC.cmake)
cmrc_add_resource_library(foo-resources
        ALIAS foo::rc
        NAMESPACE foo
        shaders/trig.vert
        shaders/trig.frag)

target_link_libraries(foo foo::rc)
#include <cmrc/cmrc.hpp>

CMRC_DECLARE(foo); // It should be the NAMESPACE property you specified
                   // in your CMakeLists.txt

int main() 
    auto fs = cmrc::foo::get_filesystem();
    auto vert_shader = fs.open("shaders/trig.vert");
    auto frag_shader = fs.open("shaders/trig.frag");
    ...
    glShaderSource(vertexShader, 1, &vert_shader.begin(), nullptr);

它可能是最容易设置和使用的库。

【讨论】:

看起来小而整洁,很棒。你知道它是否适用于交叉编译?当我第一次需要这个时,这已经过去很久了,但是交叉编译是我最初的要求之一。这可能仍然与其他正在阅读本文的人有关。 它是跨平台的,是的。 @NicuStiurca

材质着色器和纹理

...对所用纹理的引用、平铺信息、颜色色调等来定义表面应使用的渲染方式。材质的可用选项取决于材质使用的着色器。着色器是一些包含数学计算和算法的小脚本,根据光照输入和材质配置来计算每个像素渲染的颜色。纹理... 查看详情

如何将片段着色器图像更改保存到图像输出文件?

...我的项目的一部分不是由我制作的(因为它是开源的),使用Op 查看详情

如何将图像后处理着色器的结果添加到场景中

...我想在其中一些(不是全部)上应用一些后期处理效果(例如模糊、绽放)。为了实现这一点,我定义了一个带有附加纹理(颜色缓冲区)和渲染缓冲区(深度缓冲区)的屏 查看详情

(转载)(官方)ue4--图像编程----着色器开发----异步计算(asynccompute)

...可运行与渲染异步的 dispatch() 调用,有效利用未使用的GPU资源(计算单元(CU)、寄存器和带宽)。异步计算使用单独的上下文,我们通过RHI函数同步渲染和计算上下文。DrPIX可用于识别从异步计算获益的区域。例如,特... 查看详情

(转载)(官方)ue4--图像编程----着色器开发

...器开发相关的日志和警告。将更改保存到.usf文件之后,使用 Ctrl+Shift+. 可重新编译已更改的着色器。如果您更改包括在许多着色器中的 查看详情

将 UInt8 组件类型的纹理传递给 Metal 计算着色器

...,并将它们组合成UInt32并将其存储在图像的缓冲区中。我使用以下代码 查看详情

将像素着色器应用于图像

】将像素着色器应用于图像【英文标题】:Applyingpixelshaderstoimages【发布时间】:2012-10-2115:13:33【问题描述】:我想在c#/xaml中对图像应用着色器效果。我找到了这个例子:http://msdn.microsoft.com/en-us/library/system.windows.media.effects.shadere... 查看详情

我的opengl学习进阶之旅着色器编译器和程序二进制码(代码片段)

...索GL_SHADER_COMPILER值必须是GL_TRUE)。你可以指定着色器使用glShaderSource,就像我们在前面博客示例中所做的一样。你还可以尝试缓解着色器编译对资源的影响。也就是说,一旦完成了应用程序中着色器的编译,就可... 查看详情

我的opengl学习进阶之旅着色器编译器和程序二进制码(代码片段)

...索GL_SHADER_COMPILER值必须是GL_TRUE)。你可以指定着色器使用glShaderSource,就像我们在前面博客示例中所做的一样。你还可以尝试缓解着色器编译对资源的影响。也就是说,一旦完成了应用程序中着色器的编译,就可... 查看详情

opengles之如何传输一个超大数组给着色器程序(代码片段)

...个超大的数组传给着色器程序?目前常用的有三种方式:使用将数组加载到2D纹理的方式,然后使用texelFetch取数据;使用uniform缓冲区对象,即UBO;使用纹理缓冲区对象,即TBO。二、将数组加载到纹理使用将数组加载到纹理的方... 查看详情

使用着色器进行计算

】使用着色器进行计算【英文标题】:Usingshaderforcalculations【发布时间】:2011-04-2407:41:53【问题描述】:是否可以使用着色器计算一些值,然后将它们返回以供进一步使用?例如,我将网格发送到GPU,并带有一些关于如何修改(... 查看详情

webgl着色器快速教程(代码片段)

在本文中,我们将了解如何使用超过150行代码将图像渲染到页面!我知道可以只使用一个标签并完成它。但这样做是一个很好的练习,因为它迫使我们引入许多重要的WebGL概念。我最近在一个需要使用WebGL的项目上工... 查看详情

webgl着色器快速教程(代码片段)

在本文中,我们将了解如何使用超过150行代码将图像渲染到页面!我知道可以只使用一个标签并完成它。但这样做是一个很好的练习,因为它迫使我们引入许多重要的WebGL概念。我最近在一个需要使用WebGL的项目上工... 查看详情

webgl纹理

...色,组成纹理图像的像素又称为纹素.每个纹素有可以都是使用RGB或者RGBA格式编码纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色,webgl系统上的纹理坐标是二维的.webgl中使用s和t命名纹理坐标纹理坐标图... 查看详情

提高 R 光线着色器图像的分辨率

...【发布时间】:2020-03-0621:24:18【问题描述】:我正在尝试使用rayshader包制作图像。我很高兴能够使用如下代码创建png文件:library(ggplot2)library(rayshader)example_plot<-ggplot(data.frame(x=c(1,2,3),y=c(4,5,6 查看详情

纹理中的 OpenGL 片段着色器

...四边形上。但是,我想对该纹理的rgb像素进行一些操作,例如,我希望显示的颜色是原始图像的RGB像素的某个函数。我的问题是:我可以将片段着色器应用于2D纹理吗?如果可以,如何访问原始纹理图像的rgb值?任 查看详情

opengles3.0创建着色器步骤

...器对象相当于c语言的编译器(为源代码生成目标代码,例如.obj或.o文件)程序对象相当于c语言的链接程序(将对象文件链接为最后的程序)创建步骤:1创建顶点着色器对象和片段着色器对象//glCreateShader2将源代码连接到每个着... 查看详情

如何通过实例渲染将每个实例数据(例如定位)传递给 OpenGL 3.2 中的着色器?

...【发布时间】:2014-11-1517:01:18【问题描述】:我正在尝试使用实例化渲染来渲染多维数据集。我得到了这个工作,我用glDrawArraysInstanced渲染 查看详情