withUnsafeBytes + Generic type behavior

Viewed 379

I have a function that allows me to read a number (Integer, Double etc) from a binary file using generic types. For example if I expect a Int64, il will read 8 bytes...

// A simple function that read n bytes from a FileHandle and returns
// the data

public func read(chunkSize: Int) -> Data {
   return self.handle!.readData(ofLength: chunkSize)
}

// A function that reads the proper amount of bytes specified
// by the return type which in my case would be an integer 

public func readNumber<I>() -> I? {
   let data: Data = self.read(chunkSize: MemoryLayout<I>.size)
   if data.count == 0 {
       return nil
   }
   return data.withUnsafeBytes { $0.pointee }
}

The readNumber randomly returns nil for no reason. Not from the count check but from the last line.

However it perfectly works when I cast to I like so :

return data.withUnsafeBytes { $0.pointee } as I

Why is that ?

EDIT:

I reproduced this using Playgrounds :

class Test {

    public func read(chunkSize: Int) -> Data {
        return Data(repeating: 1, count: chunkSize)
    }

    public func readNumber<T>() -> T? {
        let data: Data = read(chunkSize: MemoryLayout<T>.size)
        if data.count == 0 {
            return nil
        }
        return data.withUnsafeBytes { $0.pointee }
    }

    public func example() {
        let value2: Double = readNumber()!
        print(value2)
    }
}

let test = Test()

for i in 0..<1000 {
    test.example()
}
1 Answers

Seems I need to correct my comment a little. Even when Swift works consistently as programmed, the result may seem randomly changing, when you have some memory issue like accessing out of bounds.

First prepare a magical extension for UnsafePointer:

extension UnsafePointer {
    var printingPointee: Pointee {
        print(Pointee.self) //<- Check how Swift inferred `Pointee`
        return self.pointee
    }
}

And modify your EDIT code a little:

class Test {

    public func read(chunkSize: Int) -> Data {
        return Data(repeating: 1, count: chunkSize)
    }

    public func readNumber<T>() -> T? {
        let data: Data = read(chunkSize: MemoryLayout<T>.size)
        if data.count == 0 {
            return nil
        }
        print(T.self) //<- Check how Swift inferred `T`
        return data.withUnsafeBytes { $0.printingPointee }
    }

    public func example() {
        let value2: Double = readNumber()!
        print(value2)
    }
}

let test = Test()

for _ in 0..<1000 {
    test.example()
}

Output:

Double
Optional<Double>
7.748604185489348e-304
Double
Optional<Double>

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

How many pairs of Double and Optional<Double> shown would be seemingly random, but the cause of this behavior is quite clear.

In this line return data.withUnsafeBytes { $0.printingPointee }, Swift infers the type of $0 as UnsafePointer<Optional<Double>>.

In the current implementation of Swift, Optional<Double> occupies 9 bytes in memory:

print(MemoryLayout<Optional<Double>>.size) //-> 9

So, $0.pointee accesses 9 bytes starting from the pointer, although the pointer is pointing to the region of 8-byte:

|+0|+1|+2|+3|+4|+5|+6|+7|+8|
+--+--+--+--+--+--+--+--+
 01 01 01 01 01 01 01 01 ??
 <-taken from the Data->

As you know, the extra 9th (+8) byte cannot be predictable and may seemingly be random, which is an indicator of nil in Optional<Double>.

Exactly the same inference is working in your code. In your readNumber<T>(), the return type is clearly declared as T?, so, in the line return data.withUnsafeBytes { $0.pointee }, it is very natural that Swift infers the type of $0.pointee as Double? aka Optional<Double>.

You know you can control this type inference with adding as T.

Related