将 C++ 'extern "C" __declspec(dllexport)' 结构转换为 Rust 的问题

Posted

技术标签:

【中文标题】将 C++ \'extern "C" __declspec(dllexport)\' 结构转换为 Rust 的问题【英文标题】:Problems Translating C++ 'extern "C" __declspec(dllexport)' struct to Rust将 C++ 'extern "C" __declspec(dllexport)' 结构转换为 Rust 的问题 【发布时间】:2022-01-05 16:16:29 【问题描述】:

我目前正在尝试重建和更新一个用 Rust 编写的项目(更具体地说,它是 Skyrim 的 SKSE64 插件:https://github.com/lukasaldersley/sse-mod-skyrim-search-se 来自 qbx2) 我面临的最后一个问题是库现在需要从我们的库中导出一个结构以进行版本检查。

我尝试了许多可能很愚蠢的方法来实现它,但我无法让它发挥作用。

c++代码如下:

struct SKSEPluginVersionData

    enum
    
        kVersion = 1,
    ;

    enum
    
        // set this if you are using a (potential at this time of writing) post-AE version of the Address Library
        kVersionIndependent_AddressLibraryPostAE = 1 << 0,
        // set this if you exclusively use signature matching to find your addresses and have NO HARDCODED ADDRESSES
        kVersionIndependent_Signatures = 1 << 1,
    ;

    UInt32  dataVersion;            // set to kVersion

    UInt32  pluginVersion;          // version number of your plugin
    char    name[256];              // null-terminated ASCII plugin name

    char    author[256];            // null-terminated ASCII plugin author name (can be empty)
    char    supportEmail[256];      // null-terminated ASCII support email address (can be empty)

    // version compatibility
    UInt32  versionIndependence;    // set to one of the kVersionIndependent_ enums or zero
    UInt32  compatibleVersions[16]; // zero-terminated list of RUNTIME_VERSION_ defines your plugin is compatible with

    UInt32  seVersionRequired;      // minimum version of the script extender required, compared against PACKED_SKSE_VERSION
                                    // you probably should just set this to 0 unless you know what you are doing
;

#define RUNTIME_VERSION_1_6_318 0x010613E0

extern "C" 
    __declspec(dllexport) SKSEPluginVersionData SKSEPlugin_Version =
    
        SKSEPluginVersionData::kVersion,

        1,
        "Skyrim Search",

        "qbx2",
        "",

        0,  // not version independent
         RUNTIME_VERSION_1_6_318, 0 , // RUNTIME_VERSION_1_6_318 is 

        0,  // works with any version of the script extender. you probably do not need to put anything here
    ;
;

到目前为止,我在 Rust 中的想法是:

enum KVersionenum 
    KVersion=1,


#[repr(C)]
pub struct SKSEPluginVersionData 
    dataVersion: u32,

    pluginVersion: u32,
    name: [char;256],

    author: [char;256],
    supportEmail: [char;256],

    versionIndependence: u32,
    compatibleVersions: [u32;16],

    seVersionRequired: u32,


//0x010613E0 is RUNTIME_VERSION_1_6_318

//how can I do this OUTSIDE of a method and how can I make it public to the dll? is that even possible?
let SKSEPlugin_Version = SKSEPluginVersionData 
    dataVersion: KVersionenum::KVersion as u32,
    pluginVersion: 1,
    name: "Skyrim Search\0", //this doesn't work, how can I fill this char array?
    author: "qbx2 / lukasaldersley\0", //same here
    supportEmail: "something@something.something\0", //and here
    versionIndependence: 0,
    compatibleVersions: [0x010613E0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], //I'm sure this is a horrible way of doing this
    seVersionRequired: 0,
;

当我尝试在函数之外使用 let thingy 时,编译器抱怨期待“项目”,但我的 google-fu 不够好,无法在那里找到任何有用的信息,因为我一直在寻找有关视频游戏中项目的信息生锈。

对于汽车数组/字符串问题,我遇到了 std:ffi 的东西,完全迷失在它的文档中,但据我所知,它只会处理指针,这不是我需要的。

现在的两个问题是如何填充这些 char 数组(我不能只传递一个指针)以及如何创建这个结构的实例作为全局变量(或者 Rust 称之为),因为let name = something ... 不起作用。

据我所知,导出到 dll 的函数看起来像这样,但我认为它不会以相同的方式用于该结构。

#[no_mangle]
pub extern "C" fn SKSEPlugin_Query(skse: *const SKSEInterface, info: *mut PluginInfo) -> bool ...

甚至有可能做到这一点吗?

有人可以在这里帮助我,或者至少为我指明正确的方向吗? 请注意,我是 Rust 的绝对初学者,显然错误地认为仅添加一个结构不会那么复杂。

【问题讨论】:

AFAIK,Rust 中没有导出全局变量的语法。最好的办法可能是用 C 语言编写该变量,将其编译为目标文件并将其链接到 Rust DLL 的其余部分。 @rodrigo 这不起作用:Can a Rust constant/static be exposed to C? @kmdreko: 嗯,很棘手……它声明了一个全局变量,但没有通过 dll 导出它。在 Windows 中,您需要 __declspec(export) 符号。我似乎记得您也可以在 DEF 文件中执行此操作,用作链接器的输入,甚至作为链接器的 direct argument,但我已经有一段时间没有在 Windows 上进行黑客攻击了。 【参考方案1】:

首先,Rust 中的 char 是 32 位值,而 C++ 中的 char 是 8 位值(这不是 strictly true,但 Rust 不支持不支持的架构)。所以 nameauthorsupportEmail 字段应为 u8 数组。


您可以通过在公共 static 变量上使用 #[no_mangle] 来导出全局值:

#[no_mangle]
pub static SKSEPlugin_Version: SKSEPluginVersionData = SKSEPluginVersionData 
    ...
;

见:Can a Rust constant/static be exposed to C?


您可以使用byte string 从文字初始化u8 数组并取消引用它:*b"..."。不幸的是,没有像 C++ 中那样用于对数组的未确定部分进行零填充的简写,所以你只剩下:

    name: *b"Skyrim Search\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
    author: *b"qbx2 / lukasaldersley\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
    supportEmail: *b"something@something.something\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
    compatibleVersions: [0x010613E0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],

老实说,这根本不好。您可以使用一些为您填充数组的函数来清理它,但是,初始化 static 变量需要 const 函数,这些函数在 Rust 中仍然相对不成熟,所以我们不能使用像 for loops or traits 这样的东西来帮助我们:

const fn zero_pad_u8<const N: usize, const M: usize>(arr: &[u8; N]) -> [u8; M] 
    let mut m = [0; M];
    let mut i = 0;
    while i < N 
        m[i] = arr[i];
        i += 1;
    
    m


const fn zero_pad_u32<const N: usize, const M: usize>(arr: &[u32; N]) -> [u32; M] 
    let mut m = [0; M];
    let mut i = 0;
    while i < N 
        m[i] = arr[i];
        i += 1;
    
    m


...

    name: zero_pad_u8(b"Skyrim Search"),
    author: zero_pad_u8(b"qbx2 / lukasaldersley"),
    supportEmail: zero_pad_u8(b"something@something.something"),
    compatibleVersions: zero_pad_u32(&[0x010613E0]),

仍然不是那么好,但至少它是可以管理的。可能有一个可用的 crate 可以为您执行此操作。


最后,您不必使用与 C++ 中相同的字段命名约定,因为重要的是顺序和类型,所以我建议使用 snake_case,但如果您确实想保持相同为了名称的一致性,您可以将#[allow(non_snake_case)] 属性放在SKSEPluginVersionData 上以抑制编译器警告。

我还建议为该魔法值创建一个常量,而不仅仅是一个注释:

const RUNTIME_VERSION_1_6_318: u32 = 0x010613E0;

在playground 上查看完整内容。

【讨论】:

非常感谢。我实施了您的建议,并且确实有效。我认为您可以删除“这都是未经测试的”注释,因为我现在测试了您的所有代码。 @lukasaldersley 不错!会做。很高兴这一切都解决了:)

以上是关于将 C++ 'extern "C" __declspec(dllexport)' 结构转换为 Rust 的问题的主要内容,如果未能解决你的问题,请参考以下文章

将 C++ 中的“extern C”与 ctypes 的向量一起使用

在 C++ 中使用 C 函数 使用 extern "C"

从 C++ 链接到 C 库:为啥不总是需要 extern?

extern "c"的作用详解

尝试使用 c++ .so 库编译 c 代码,带有 extern "C" ... 段

extern "C"