如何在C二进制文件中嵌入Lua脚本?

8

我在shell环境中得到了很好的待遇,可以进行以下操作:

./lua <<EOF
> x="hello world"
> print (x)
> EOF
hello world

现在我正在尝试将一个Lua脚本包含在一个C应用程序中,我希望这个应用程序会随着时间的推移而变得更加完善。我从简单的开始:

const char *lua_script="x=\"hello world\"\n"
  "print(x)\n";
luaL_loadstring(L, lua_script);
lua_pcall(L, 0, 0, 0);

但这样做有几个缺点。主要是,我必须转义换行符和引号。但现在我在使用gcc编译时遇到了“字符串长度‘1234’大于长度‘509’,ISO C90编译器需要支持”的警告,并且我希望保持这个程序不仅自包含而且可移植到其他编译器。

如何最好地将一个大的Lua脚本包含在C程序中,而不作为单独的文件发送给最终用户?理想情况下,我想将脚本移动到一个单独的*.lua文件中以简化测试和更改控制,并将该文件编译到可执行文件中。


1
我会禁用那个警告,或者明确告诉gcc你想使用-std=c99编写现代(C99)的C代码。C99要求编译器支持长度达到4095个字符的字符串字面量。然而,在实践中,任何值得信赖的非嵌入式环境编译器都不会对字符串长度有任何限制,因此这个警告应该被禁用或忽略。 - R.. GitHub STOP HELPING ICE
1
关于转义字符、换行符等问题,我会编写一个辅助程序/脚本将文本文件转换为有效的C字符串,并将其作为构建过程的一部分。 - R.. GitHub STOP HELPING ICE
将脚本定义更改为:const char lua_script[] = "..."; 以避免不必要的指针。 - Jonathan Leffler
@B Mitch:不要假设它们是相同的,它们非常不同(尽管在许多情况下,引用它们的净结果是相同的)。char *为指针和字符串初始化程序分配空间(该指针可以更改);char []为字符串初始化程序分配空间,并且有一个地址可以传递给函数,但无法更改并且不占用任何空间。 - Jonathan Leffler
@Jonathan Leffler:有趣,谢谢!@R..:最终我选择了在使用bin2c时添加-std=c99,因为编译器已经开始提示sqlite源代码中的一个long long问题。 - BMitch
显示剩余2条评论
2个回答

13
在支持binutils的系统上,你也可以使用'ld -r'将Lua文件“编译”成.o文件,然后将.o链接到共享对象中,最后将应用程序链接到共享库。运行时,您可以使用dlsym(RTLD_DEFAULT,...)在Lua文本中进行符号查找,并随意评估它。
要从some_stuff.lua创建some_stuff.o:
ld -s -r -o some_stuff.o -b binary some_stuff.lua
objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents some_stuff.o some_stuff.o

这将生成一个带有符号的目标文件,其中包括了您的Lua数据的开始、结束和大小。就我所知,这些符号是由ld根据文件名确定的。您无法控制这些名称,但它们会得到一致的推导。您将得到如下内容:

$ nm some_stuff.o 
000000000000891d R _binary_some_stuff_lua_end
000000000000891d A _binary_some_stuff_lua_size
0000000000000000 R _binary_some_stuff_lua_start

现在将some_stuff.o与其他目标文件一样链接到共享对象中。然后,在您的应用程序中编写一个函数,将名称“some_stuff_lua”作为参数,并执行适当的dlsym操作。以下是C++示例代码,假设您有一个名为SomeLuaStateWrapper的lua_State包装器:
void SomeLuaStateWrapper::loadEmbedded(const std::string& embeddingName)
{
    const std::string prefix = "_binary_";
    const std::string data_start = prefix + embeddingName + "_start";
    const std::string data_end = prefix + embeddingName + "_end";

    const char* const data_start_addr = reinterpret_cast<const char*>(
        dlsym(RTLD_DEFAULT, data_start.c_str()));

    const char* const data_end_addr = reinterpret_cast<const char*>(
        dlsym(RTLD_DEFAULT, data_end.c_str()));

    THROW_ASSERT(
        data_start_addr && data_end_addr,
        "Couldn't obtain addresses for start/end symbols " <<
        data_start << " and " << data_end << " for embedding " << embeddingName);

    const ptrdiff_t delta = data_end_addr - data_start_addr;

    THROW_ASSERT(
        delta > 0,
        "Non-positive offset between lua start/end symbols " <<
        data_start << " and " << data_end << " for embedding " << embeddingName);

    // NOTE: You should also load the size and verify it matches.

    static const ssize_t kMaxLuaEmbeddingSize = 16 * 1024 * 1024;
    THROW_ASSERT(
        delta <= kMaxLuaEmbeddingSize,
        "Embedded lua chunk exceeds upper bound of " << kMaxLuaEmbeddingSize << " bytes");

    namespace io = boost::iostreams;
    io::stream_buffer<io::array_source> buf(data_start_addr, data_end_addr);
    std::istream stream(&buf);

    // Call the code that knows how to feed a
    // std::istream to lua_load with the current lua_State.
    // If you need details on how to do that, leave a comment
    // and I'll post additional details.
    load(stream, embeddingName.c_str());
}

因此,在您的应用程序中,假设您已经链接或dlopen了包含some_stuff.o的库,您只需说:

SomeLuaStateWrapper wrapper;
wrapper.loadEmbedded("some_stuff_lua");

如果你想让包含some_stuff.lua的共享库能够通过Lua的“require”加载,那么只需在另一个C/C++文件中给包含some_stuff.o的同一库添加一个luaopen入口点:

这样,在'wrapper'上下文中,一些stuff.lua的原始内容将被lua_load加载。

extern "C" {

int luaopen_some_stuff(lua_State* L)
{
    SomeLuaStateWrapper wrapper(L);
    wrapper.loadEmbedded("some_stuff_lua");
    return 1;
}

} // extern "C"

你的嵌入式Lua现在也可以通过require来使用了。这在使用luabind时特别方便。
使用SCons,很容易让构建系统知道当它看到共享库源代码部分中的.lua文件时,应该使用上述ld/objcopy步骤“编译”该文件。
# NOTE: The 'cd'ing is annoying, but unavoidable, since
# ld in '-b binary' mode uses the name of the input file to
# set the symbol names, and if there is path info on the
# filename that ends up as part of the symbol name, which is
# no good. So we have to cd into the source directory so we
# can use the unqualified name of the source file. We need to
# abspath $TARGET since it might be a relative path, which
# would be invalid after the cd.

env['SHDATAOBJCOM'] = 'cd $$(dirname $SOURCE) && ld -s -r -o $TARGET.abspath -b binary $$(basename 
$SOURCE)'
env['SHDATAOBJROCOM'] = 'objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents $
TARGET $TARGET'

env['BUILDERS']['SharedLibrary'].add_src_builder(
    SCons.Script.Builder(
        action = [
            SCons.Action.Action(
                "$SHDATAOBJCOM",
                "$SHDATAOBJCOMSTR"
                ),
                SCons.Action.Action(
                "$SHDATAOBJROCOM",
                "$SHDATAOBJROCOMSTR"
                ),
            ],
            suffix = '$SHOBJSUFFIX',
            src_suffix='.lua',
            emitter = SCons.Defaults.SharedObjectEmitter))

我确定其他现代构建系统,如CMake,也可以做到这样的事情。
当然,这种技术不仅限于Lua,而且可以用于在二进制文件中嵌入几乎任何资源。

2
如果我需要一些不特定于Lua且可移植性不是那么重要的东西,我会将这个解决方案记在我的后袋里。 - BMitch

4

一种非常便宜但不太容易更改的方法是使用类似于bin2c的工具从选定的lua文件(或其编译后的字节码,速度更快且更小)生成头文件,然后将其传递给lua执行。

您还可以尝试将其嵌入为资源,但我不知道在visual studio / windows之外如何操作。

根据您想要做什么,您甚至可能会发现exeLua有用。


从5.1版本开始,我在发行版中没有看到bin2c,但我搜索了一下,找到了http://lua-users.org/wiki/BinToCee,看起来我可以修改它以适应我的需求。感谢Necrolis! - BMitch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接