Swift 转换 C 的 uint64_t 不同于它使用自己的 UInt64 类型

Swift converts C's uint64_t different than it uses its own UInt64 type

我正在将应用程序从 (Objective-)C 移植到 Swift,但必须使用用 C 编写的 third-party 框架。有几个不兼容,例如被解释为 Int 但必须作为 UInts 等传递给框架函数的 typedef。因此,为了避免在整个 Swift 应用程序中不断进行转换操作,我决定将 C header 文件传输到 Swift,将所有类型都放在一个地方。

我几乎可以转移所有东西并克服了很多障碍,但是这个:

C header 定义了一个包含 uint64_t 变量的结构。此结构用于将数据作为指针传输到回调函数。回调函数采用 void 指针作为参数,我必须使用 UnsafeMutablePointer 操作将其转换为结构类型(或 header 的另一个结构,如果适用)。只要我使用在导入时由 Swift 自动转换的 C header 中的原始结构,所有转换和 memory-accessing 都可以正常工作。

但是在 Swift 中手动复制结构不会 "byte-fit"。

让我向您展示这种情况的简化示例:

在 CApiHeader.h 文件中有类似

的东西
typedef struct{
  uint32_t var01;
  uint64_t var02;
  uint8_t arr[2];
}MyStruct, *MyStructPtr;

根据我的理解,这里应该是 Swift 等价物

struct MyStruct{
  var01: UInt32
  var02: UInt64
  arr: (UInt8, UInt8)
}

或者这个元组符号也应该起作用

typealias MyStruct = (
  var01: UInt32,
  var02: UInt64,
  arr: (UInt8, UInt8)
)

这可以正常工作,但一旦有 UInt64 类型就不行了。

好的,那会发生什么?

将指针转换为我自己的 Swift MyStruct 实现,孔数据从 UInt64 字段开始移动 2 个字节。所以在这个例子中,两个 arr 字段都不在正确的位置,但在 UInt64 位中,数字应该是 64。所以它接缝 UInt64 字段只有 48 位。

这符合我的观察,如果我用这个替代方案替换 UIn64 变量

struct MyStruct{
  var01: UInt32
  reserved: UInt16
  var02: UInt32
  arr: (UInt8, UInt8)
}

或者这个

struct MyStruct{
  var01: UInt32
  var02: (UInt32, UInt32)
  arr: (UInt8, UInt8)
}

(或等效的元组表示法)它正确对齐 arr 字段。 但是你可以很容易地猜到 var02 包含不能直接使用的数据,因为它被分割成多个地址范围。第一个选择更糟,因为它接缝 Swift 填补了 reserved 字段和 var02 字段之间的空白16 位 - 我上面提到的丢失/移位的 2 个字节 - 但这些不容易访问。

所以我还没有想出 Swift 中 C 结构的任何等效转换。

这里到底发生了什么,Swift 实际上是如何从 C header 转换结构的?

你们有什么提示或解释甚至解决方案吗?

更新

C 框架有一个 API 带有此签名的函数:

int16_t setHandlers(MessageHandlerProc messageHandler);

MessageHandlerProc 是过程类型:

typedef void (*messageHandlerProc)(unsigned int id, unsigned int messageType, void *messageArgument);

所以 setHandlers 是框架内的一个 C 过程,它获取指向回调函数的指针。此回调函数必须提供一个 void 指针的参数,该参数被强制转换为例如

typedef struct {
    uint16_t        revision;
    uint16_t        client;
    uint16_t        cmd;
    int16_t         parameter;
    int32_t         value;
    uint64_t        time;
    uint8_t         stats[8];
    uint16_t        compoundValueOld;
    int16_t         axis[6];
    uint16_t        address;
    uint32_t        compoundValueNew;
} DeviceState, *DeviceStatePtr;

Swift 足够聪明,可以使用 convention(c) 语法导入 messageHandlerProc,因此可以直接使用过程类型。另一方面,不可能使用标准的 func 语法并将我的 messageHandler 回调函数位播到这种类型。所以我使用闭包语法来定义回调函数:

let myMessageHandler : MessageHandlerProc = { (deviceID : UInt32, msgType : UInt32, var msgArgPtr : UnsafeMutablePointer<Void>) -> Void in

...

}

我将上面提到的结构转换成我原来的不同结构post。

并且不!stats 定义为 Swift 数组不起作用。 Swift 中的数组不等同于 C 中的数组,因为 Swift 的数组是扩展类型。用指针写入和读取它会导致异常

只有元组在 Swift 中本地实现,您可以 运行 来回使用指针。

好的...一切正常,只要有数据,我的回调函数就会被调用。

所以在 myMessageHandler 里面,我想使用 msgArgPtr 里面存储的数据,它是一个空指针,因此必须转换成 设备状态.

let state = (UnsafeMutablePointer<MyDeviceState>(msgArgPtr)).memory

访问状态就像:

...
print(state.time)
print(state.stats.0)
...

每当我使用 DeviceState 的自动生成的 Swift 吊坠时,它都运行良好。时间变量具有 Unix 时间戳和以下统计信息(可使用元组语法访问!!!)都是它们所属的地方。

然而,使用我手动实现的结构会导致完全没有意义的时间戳值,并且统计字段向左移动(朝向 time 字段 - 这可能就是时间戳的原因value 是无用的,因为它包含来自统计数据的位 "array")。所以在他最后两个统计字段我从 compoundValueOld 和第一个 axis 字段获取值 - 当然所有溢出。

只要我愿意牺牲 time 值并通过两个 UInt32 类型的元组或将其更改为 UInt32 类型并添加一个来更改 UInt64 变量time 之前 UInt16 类型的辅助变量,我收到一个 stats "array" 并且对齐正确。

祝你有美好的一天! :-)

马丁

你的两个定义不等价。数组与元组不同。您的 C struct 给出了 24 个字节(请参阅 this question 了解原因)。 Swift 中的大小取决于您如何实现它:

struct MyStruct1 {
    var var01: UInt32
    var var02: UInt64
    var arr: (UInt8, UInt8)
}

typealias MyStruct2 = (
    var01: UInt32,
    var02: UInt64,
    arr: (UInt8, UInt8)
)

struct MyStruct3 {
    var var01: UInt32
    var var02: UInt64
    var arr: [UInt8] = [0,0]
}

print(sizeof(MyStruct1)) // 18
print(sizeof(MyStruct2)) // 18
print(sizeof(MyStruct3)) // 24, match C's

这是在阅读您更新后的问题并进行更多试验后对我之前的回答进行的更新。我认为问题是导入的 C 结构与您在 Swift 中手动实现的结构之间存在对齐差异。这个问题可以通过使用 C 辅助函数从 void 指针获取 C 结构的实例来解决,就像昨天建议的那样,然后可以将其转换为手动实现的 Swift 结构。

在创建您的 DeviceState 结构的简化模型后,我已经能够重现该问题

typedef struct
{
    uint16_t        revision;
    uint16_t        client;
    uint16_t        cmd;
    int16_t         parameter;
    int32_t         value;
    uint64_t        time;
    uint8_t         stats[8];
    uint16_t        compoundValueOld;
} APIStruct;

对应的手工Swift原生结构为:

struct MyStruct
{
    init( _apis : APIStruct)
    {
        revision = _apis.revision
        client = _apis.client  
        cmd = _apis.cmd        
        parameter = _apis.parameter
        value = _apis.value
        time = _apis.time
        stats = _apis.stats
        compoundValueOld = _apis.compoundValueOld
    }

    var     revision : UInt16
    var     client : UInt16          
    var     cmd : UInt16             
    var     parameter : Int16
    var     value : Int32
    var     time : UInt64
    var     stats : (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8);
    var     compoundValueOld : UInt16
}

您正在使用的 C 框架可能是使用不同的结构打包编译的,导致对齐不匹配。我用了

#pragma pack(2) 

在我的 C 代码中打破 Swift 的本机和导入的 C 结构之间的位匹配。

如果我做类似的事情

func swiftCallBackVoid( p: UnsafeMutablePointer<Void> )
{
     ...
    let _locMS:MyStruct = (UnsafeMutablePointer<MyStruct>(p)).memory
     ...
}

_locMS 中的数据与 C 代码放置在那里的数据不同。只有在我的 C 代码中使用 pragma 更改结构包装时才会出现此问题;如果使用默认对齐方式,上述不安全转换工作正常。解决这个问题的方法如下:

let _locMS:MyStruct = MyStruct(_apis: (UnsafeMutablePointer<APIStruct>(p)).memory)

顺便说一句,Swift 导入 C 结构的方式,数组成员变成了元组;从必须使用元组符号来访问它们这一事实可以看出这一点 Swift.

我有一个示例 Xcode 项目说明了我放在 github:

上的所有这些
https://github.com/omniprog/xcode-samples

显然,使用辅助 C 函数从 void 指针获取 APIStruct 然后将 APIStruct 转换为 MyStruct 的方法可能是也可能不是一个选项,具体取决于结构的使用方式、它们的大小,以及应用程序的性能要求。如您所知,此方法涉及对结构的一些复制。我认为其他方法包括在 Swift 代码和第 3 方 C 框架之间编写一个 C 层,研究 C 结构的内存布局并以创造性的方式访问它(可能很容易破坏),使用导入的 C在您的 Swift 代码等中更广泛地构建...

这是一种在 C 和 Swift 代码之间共享数据的方法,无需不必要的复制,并且在 Swift 中所做的更改对 C 代码可见。但是,使用以下方法时,必须了解对象生命周期和其他内存管理问题。可以创建一个 class 如下:

// This typealias isn't really necessary, just a convenience
typealias APIStructPtr = UnsafeMutablePointer<APIStruct>

struct MyStructUnsafe
{
    init( _p : APIStructPtr )
    {
        pAPIStruct = _p
    }

    var time: UInt64 {
        get {
            return pAPIStruct.memory.time
        }
        set( newVal ) {
            pAPIStruct.memory.time = newVal
        }
    }
    var   pAPIStruct: APIStructPtr
}

那么我们可以使用这个结构如下:

func swiftCallBackVoid( p: UnsafeMutablePointer<Void> )
{
   ...
   var _myUnsafe : MyStructUnsafe = MyStructUnsafe(_p: APIStructPtr(p))
   ... 
   _myUnsafe.time = 9876543210  // this change is visible in C code!
   ...
}