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

依然范特西╮ 提交于 2019-12-18 06:48:08

问题


I am in the process of porting an application from (Objective-)C to Swift but have to use a third-party framework written in C. There are a couple of incompatibilities like typedefs that are interpreted as Int but have to be passed to the framework's functions as UInts or the like. So to avoid constant casting operations throughout the entire Swift application I decided to transfer the C header files to Swift, having all types as I I need them to be in one place.

I was able to transfer nearly everything and have overcome a lot of hurdles, but this one:

The C header defines a struct which contains a uint64_t variable among others. This struct is used to transfer data to a callback function as a pointer. The callback function takes a void pointer as argument and I have to cast it with the UnsafeMutablePointer operation to the type of the struct (or another struct of the header if appropriate). All the casting and memory-accessing works fine as long as I use the original struct from the C header that was automatically transformed by Swift on import.

Replicating the struct manually in Swift does not "byte-fit" however.

Let me show you a reduced example of this situation:

Inside the CApiHeader.h file there is something like

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

From my understanding this here should be the Swift equivalent

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

Or what should also work is this tuple notation

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

This works normally, but not as soon as there is an UInt64 type.

Okay, so what happens?

Casting the pointer to one of my own Swift MyStruct implementations the hole data is shifted by 2 bytes, starting at the UInt64 field. So in this example the both arr fields are not at the correct position, but inside the UInt64 bits, that should be 64 in number. So it seams that the UInt64 field has only 48 bits.

This accords to my observation that if I replace the UIn64 variable with this alternative

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

or this one

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

(or the equivalent tuple notation) it aligns the arr fields correctly. But as you can easily guess var02 contains not directly usable data, because it is split over multiple address ranges. It is even worse with the first alternative, because it seams that Swift fills up the gap between the reserved field and the var02 field with 16 bits - the missing / shifted 2 bytes I mentioned above - but these are not easily accessible.

So I haven't figured out any equivalent transformation of the C struct in Swift.

What happens here exactly and how does Swift transforms the struct from the C header actually?

Do you guys have a hint or an explanation or even a solution for me, please?

Update

The C framework has an API function with this signature:

int16_t setHandlers(MessageHandlerProc messageHandler);

MessageHandlerProc is procedure type:

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

So setHandlers is a C procedure inside the framework that gets a pointer to a callback function. This callback function has to provide an argument of a void Pointer, that gets casted to e.g.

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 is smart enough to import the messageHandlerProc with the convention(c) syntax, so the procedure type is directly available. On the other hand it is not possible use the standard func syntax and bitcast my messageHandler callback function to this type. So I used the closure syntax to define the callback function:

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

...

}

I converted the above mentioned structure into the different structures of my original post.

And No! Defining stats as Swift Array does not work. An Array in Swift in not equivalent to an Array in C, because Swift's Array is a extended type. Writing to and reading from it with a pointer causes an exception

Only Tuples are natively implemented in Swift and you can run back and forth with pointers over it.

Okay... this works all fine and my callback function gets called whenever data is available.

So inside myMessageHandler I want to use the stored Data inside msgArgPtr which is a void pointer and thus has to be cast into DeviceState.

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

Accessing state it like:

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

Whenever I use the automatically generated Swift pendant of DeviceState it all works nicely. The time variable has the Unix Time Stamp and the following stats (accessible with tuple syntax!!!) are all where they belong.

Using my manually implemented struct however results in a completely senseless time stamp value and the stats fields are shifted to the left (towards the time field - that's probably why the time stamp value is useless, because it contains bits from the stats "array"). So in the last two fields of stats I get values from compoundValueOld and the first axis field - with all the overflowing of course.

As long as I am willing to sacrifice the time value and change the UInt64 variable by either a tuple of two UInt32 types or by changing it to a UInt32 type and adding a auxiliary variable of the type UInt16 right before time, I receive a stats "array" with correct alignment.

Have a nice day! :-)

Martin


回答1:


This is an update to my earlier answer after reading your updated question and experimenting some more. I believe the problem is an alignment discrepancy between the imported C structure and the one you manually implemented in Swift. The problem can be solved by using a C helper function to get an instance of the C struct from void pointer as was suggested yesterday, which can then be converted to the manually implemented Swift struct.

I've been able to reproduce the problem after creating an abbreviated mock-up of your DeviceState structure that looks like

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;

The corresponding hand-crafted Swift native structure is:

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
}

The C framework you are working with could have been compiled using a different struct packing, resulting in a non-matching alignment. I used

#pragma pack(2) 

in my C code to break the bit-matching between the Swift's native and imported C struct.

If I do something like

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

the data in _locMS is different from what was placed there by C code. This problem only occurs if I change struct packing using a pragma in my C code; the above unsafe conversion works fine if the default alignment is used. One can solve this problem as follows:

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

BTW, the way Swift imports the C struct, the array members become tuples; this can be seen from the fact that tuple notation has to be used to access them in Swift.

I have a sample Xcode project illustrating all this that I've placed on github:

https://github.com/omniprog/xcode-samples

Obviously, the approach of using a helper C function to get APIStruct from a void pointer and then converting the APIStruct to MyStruct may or may not be an option, depending on how the structures are used, how large they are, and on the performance requirements of the application. As you can tell, this approach involves some copying of the structure. Other approaches, I think, include writing a C-layer between Swift code and the 3rd party C framework, studying the memory layout of the C structure and accessing it in creative ways (may break easily), using the imported C struct more extensively in your Swift code, etc...

Here is a way to share data between C and Swift code without unnecessary copying and with changes made in Swift visible to C code. With the following approach, however, it's imperative to be aware of object lifetime and other memory management issues. One can create a class as follows:

// 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
}

Then we can use this structure as follows:

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



回答2:


Your two definitions are not equivalent. An array is not the same as a tuple. Your C struct gives 24 bytes (see this question as to why). The size in Swift differs depend on how you implement it:

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


来源:https://stackoverflow.com/questions/33424316/swift-converts-cs-uint64-t-different-than-it-uses-its-own-uint64-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!