Validatable can be used in other places as well, making unsafe programming much safer overall. Consider this example:
// sample user structure
struct S {
var b: Bool = true // offset 0, size 1
var int: Int32 = 0x12345678 // offset 4, size 4
var c: NSObject = NSObject() // offset 8, size 8
}
In the following code I'm deliberately messing with the underlying bytes simulating a developer error (e.g. due to a typo, lack of knowledge, lack of sleep, etc):
var s = S()
withUnsafeMutableBytes_analogue(&s) { p in
p[7] = 0xFF // messing with integer value - ok
// uncomment one of the following to trigger precondition failure:
// p[0] = 123 // mess with Bool
// p[8] |= 1 // mess with the pointer: make it odd
// memset(p + 8, 0xF0, 8); // mess with the pointer: make it bad
// memset(p + 8, 0, 8) // mess with the pointer: make it null
}
print("next line")
In all of the uncommented examples the precondition failure was triggered and the app safely terminated right away before the app reaches "next line" - a very nice behaviour compared to what could happen without the checks in-place.
For this user structure the following code is supposed to be autogenerated by the compiler:
// should be automatically generated
extension S: Validatable {
static func isValid(_ v: UnsafeRawPointer) -> Bool {
isBoolValid((v + 0).assumingMemoryBound(to: UInt8.self).pointee) &&
isObjectValid((v + 8).assumingMemoryBound(to: Int.self).pointee)
// won't check Int - all Int bit patterns are valid
}
}
Where Validatable
protocol is slightly changed compared to the version in one of the messages above:
protocol Validatable {
static func isValid(_ v: UnsafeRawPointer) -> Bool
}
And validity checks themselves for specific library types are shown here.
func isBoolValid(_ value: UInt8) -> Bool {
if value == 0 || value == 1 { return true }
print("strange bool value: \(value)")
return false
}
func isObjectValid(_ object: Int) -> Bool {
if object == 0 {
print("object == nil -> invalid object")
return false
}
if (object & 0x0F) != 0 {
print("object is not aligned properly -> invalid object")
return false
}
let size = objectSize(object)
if size == 0 {
print("object size is 0 -> invalid object")
return false
}
if size < 16 {
print("object size is too small -> invalid object")
return false
}
if objectClass(object) == nil {
print("not an object")
return false
}
return true
}
This is a quick prototype of withUnsafeMutableBytes_analogue
(obviously in this simple form it lacks all the bells and whistles like throw / re throw, etc). I'm using the C-helper API withUnsafeMutableBytesInternal
(see below).
func withUnsafeMutableBytes_analogue<T: Validatable>(_ v: UnsafeMutablePointer<T>, execute block: @escaping (UnsafeMutablePointer<UInt8>) -> Void) {
withUnsafeMutableBytesInternal(v, block)
#if DEBUG // release? controllable via diagnostic/sanitising options?
precondition(T.isValid(v), "invalid value")
#endif
}
Finally some simple C helpers.
// C.h
typedef void (^BytesBlock)(unsigned char* _Nonnull);
void withUnsafeMutableBytesInternal(void* _Nonnull value, BytesBlock _Nonnull block);
long objectSize(long object);
Class _Nullable objectClass(long object);
// C.m
// compile with -fno-objc-arc
#include "C.h"
#include <objc/runtime.h>
#include <malloc/malloc.h>
void withUnsafeMutableBytesInternal(void* value, BytesBlock block) {
block(value);
}
long objectSize(long object) {
return malloc_size((void*)object);
}
Class objectClass(long object) {
return object_getClass((void*)object);
}
As this discussion progresses it is now clear to me that this is a much bigger safety measure than just one particular check in one particular API. Let me change the subject to match the current vector of discussion.