Inconsistent treatment bewtween Swift pointer parameters and C ones

C functions that take pointers get treated differently than Swift functions that take pointers.

C ones get treated as inout requests accross the board, where ones from Swift do not. That means Swift functions that take UnsafePointers can take let arrays (but not single values?) but C functions that take UnsafePointers get put in an inout penalty box and get upscaled to needing to be UnsafeMutatingPointers (var) as far as the Swift is concerned.

I'd rather the C and Swift individual lets get treated like the Swift Arrays do, but at the very least I'd like to understand the inconsistencies.

Test case pasted below and available in context at: FixedSizeCollection/Tests/FixedSizeCollectionTests/TestingWithC.swift at dev_prefixOperators · carlynorama/FixedSizeCollection · GitHub

    func testPointerPassing() {
        //swift test --filter WithCTests.testPointerPassing
        func takesAnUnsafePointer(_ p: UnsafePointer<CInt>?)  {
            // ...
        func takesAnUnsafeMutablePointer(_ p: UnsafeMutablePointer<CInt>?)  {
            // ...
        //void acknowledge_int_buffer(int* array, const size_t n);
        //void acknowledge_int_buffer_const(const int* values, const size_t n);
        let constIntArray: [CInt] = [1, 2, 3]
        let constIntAlone: CInt = 42
        var mIntArray: [CInt] = [2, 4, 6]
        var mIntAlone: CInt = 84
        // ---------------------------------------------------------------------
        // ----------------------- const + swift
        //takesAnUnsafePointer(constIntAlone) //<= failed when array didn't
        takesAnUnsafePointer(constIntArray) //pass
        //takesAnUnsafeMutablePointer(&constIntAlone) //expected fail
        //takesAnUnsafeMutablePointer(&constIntArray) //expected fail
        // ---------------------------------------------------------------------
        // ----------------------- mutable + swift
        //takesAnUnsafePointer(mIntAlone) //<= failed when array didn't
        takesAnUnsafePointer(mIntArray)  //pass
        takesAnUnsafeMutablePointer(&mIntAlone) //<= inconsistent but appreciated pass
        takesAnUnsafeMutablePointer(&mIntArray) //pass
        // ---------------------------------------------------------------------
        // ----------------------- const + C
        //void acknowledge_cint_buffer_const(const int* values, const size_t n);
        //error: cannot convert value of type 'CInt' (aka 'Int32')
        //to expected argument type 'UnsafePointer<Int32>?'
        //acknowledge_cint_buffer_const(constIntAlone, 1) //<= consistent fail
        //error: cannot pass immutable value as inout argument:
        //'constIntAlone' is a 'let' constant
        acknowledge_cint_buffer_const(constIntArray, 3) //<= C treated as inout
        //                                                //   unlike the Swift
        //error: cannot pass immutable value as inout argument:
        //'constIntArray'/'constIntAlone' is a 'let' constant
        //(&constIntAlone, 1) //expected fail
        //acknowledge_cint_buffer(&constIntArray, 3) //expected fail
        // ---------------------------------------------------------------------
        // ----------------------- mutable + C
        //error: cannot convert value of type 'CInt' (aka 'Int32')
        //to expected argument type 'UnsafePointer<Int32>?'
        //acknowledge_cint_buffer_const(mIntAlone, 1) //<= consistent fail
        acknowledge_cint_buffer_const(mIntArray, 3) //<= newly unexpected fail
        acknowledge_cint_buffer(&mIntAlone, 1) //<= inconsistent but appreciated pass
        acknowledge_cint_buffer(&mIntArray, 3)
        XCTAssertEqual(5, 5, "everything's fine.")

I straightened your test and didn't find any inconsistencies between swift & C. Below tries all possible permutations (32 = Value|Array * let|var * const|mutable * swift|C * with & | without &) the lines that caused compilation errors are commented out, and as you can see there are no cases when C compiles and swift doesn't or vice versa.

func testPointerPassing() {
    func swift_takesConstPointer(_ p: UnsafePointer<CInt>)  {}
    func swift_takesMutablePointer(_ p: UnsafeMutablePointer<CInt>) {}
//    void c_takesMutablePointer(int * _Nonnull);
//    void c_takesConstPointer(const int * _Nonnull);

    let constValue: CInt = 42
    var mutableValue: CInt = 84
    let constArray: [CInt] = [1, 2, 3]
    var mutableArray: [CInt] = [2, 4, 6]

//    swift_takesConstPointer(constValue)
//    c_takesConstPointer(constValue)
//    swift_takesConstPointer(&constValue)
//    c_takesConstPointer(&constValue)
//    swift_takesMutablePointer(constValue)
//    c_takesMutablePointer(constValue)
//    swift_takesMutablePointer(&constValue)
//    c_takesMutablePointer(&constValue)
//    swift_takesConstPointer(mutableValue)
//    c_takesConstPointer(mutableValue)
//    swift_takesMutablePointer(mutableValue)
//    c_takesMutablePointer(mutableValue)

//    swift_takesConstPointer(&constArray)
//    c_takesConstPointer(&constArray)
//    swift_takesMutablePointer(constArray)
//    c_takesMutablePointer(constArray)
//    swift_takesMutablePointer(&constArray)
//    c_takesMutablePointer(&constArray)
//    swift_takesMutablePointer(mutableArray)
//    c_takesMutablePointer(mutableArray)

Thank you @tera for taking the time to do that. I will start a new project to recreate, because that's an interesting test.

It is, however, not the same test. Notice the Swift functions I tested were optional pointers. That is identical to the apple documentation. I had included a link to that top of the code, but I should have called it out.

I work with a lot of very old C code which have no nullability flags Adding a nullability flag anywhere means it will have to be everywhere. I'm afraid that's not a fix I can feasibly do with the code bases I work with.

I don't think it's unreasonable to think UnsafePointer<CInt>? isn't totally detached from const int*

I reran my test with nullability reintroduced (i.e. _Nullable removed from C and "?" added to swift) and got the same results, so no, it doesn't matter. Try finding when my test and your test disagree and focus on that difference to see the issue.

Note that it is possible use the NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END brackets to mark all pointers in a bunch of C declarations non_null if that's easier; but as per above – nullability shouldn't matter in this particular case.

1 Like

Thank you. I will.

I'm writing a package that has all the flags and all the functions together and then I will compare that stand alone package to my old code and what you got.

Thank you, that could end up being helpful in places where the C is under my control.

Let us know what you find.

As far as your bridging header is concerned it is, e.g:

// bridgingHeader.h
#include "existing_c_header_unmodified.h"

Things could become hairy when you need some of the pointers nullable and the rest non nullable (or vice versa). Whatever the default behaviour is (either nullable or non-nullable via NS_ASSUME_NONNULL_BEGIN/END brackets), if you need to opt-out of that default you'd need to mark those exceptions explicitly (_Nullable or _Nonnull respectively).

1 Like

Okay everything works exactly the same for me as it did for you in the new package. Zero inconsistencies.

Bizzarely, two things that didn't work before in the old package now do:
acknowledge_cint_buffer_const(constIntArray, 3)
acknowledge_cint_buffer_const(mIntArray, 3)

void acknowledge_cint_buffer_const(const int* values, const size_t n);

I'm this close to starting a new repo with only old-style C on an entirely different computer to see if putting the flags in the old code and getting the warnings changed something in the compiler automagically. I also switch back and forth between Xcode and the package manger compiling the tests, but that shouldn't be it. Those don't cross contaminate. Right?

Maybe tomorrow. But in the mean time I must have been mistaken and I apologize for crying wolf! Thank you for all your help!