Pointer to pointer in C++ is imported as `?!` in Swift when using SWIFT_UNSAFE_REFERENCE

Hi there,
I'm trying to use RocksDB from swift after C++ interoperability landed in 5.9.

I added support to build an XCFramework and added the import_as_ref attribute to the DB class, as it has a deleted copy constructor and it is an abstract base class with a single implementation.

This is from RocksDB's setting started:

#include <assert>
#include "rocksdb/db.h"

rocksdb::DB* db;
rocksdb::Options options;
options.create_if_missing = true;
rocksdb::Status status =
  rocksdb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());

The Open method has the following signature:

static Status Open(const Options& options, const std::string& name, DB** dbptr);

The problem is that it's getting imported into Swift like this:

public class func Open(_ options: rocksdb.Options, _ name: std.string, _ dbptr: rocksdb.DB?!) -> rocksdb.Status

I would expect that the last parameter to be imported as inout rocksDB.DB?

I tested both with Swift 5.9.2 and a Swift 5.10 snapshot from Dec 19.

Is there an attribute or workaround (other than creating a wrapper inside RocksDB) to make this work? Sorry if this is a known issue, I went through the interoperability docs but didn't find anything.

I'm happy to share an Xcode project with the framework integrated that demonstrates the issue.

Thanks!

1 Like

Can’t you use _Nonnull and _Nullable to fix this? Like T *_Nullable *_Nonnull, or T *_Nonnull *_Nullable, or whichever version is correct?

If you don’t control the function, though, there’s probably not much you can do about it.

1 Like

You might be able to use API-notes to abuse your way through this if you don't want to modify the source directly: API Notes: Annotations Without Modifying Headers — Clang 18.0.0git documentation

The test-cases are thus far the clearest example of doing all the bad things that I've found. With some finessing, something like this entry from the test might fit your needs?

  - Name: OverriddenTypes
    Functions:
      - Selector: "methodToMangle:second:"
        MethodKind: Instance
        ResultType: 'char *'
        Parameters:
          - Position: 0
            Type: 'SOMEKIT_DOUBLE *'
          - Position: 1
            Type: 'float *'
3 Likes

Thanks a lot to both!

API-notes seem very promising to avoid modifying the headers, which is a bit inconvenient in this case as I'd need to fork and sync changes, releases, etc..

That said, I'd be happy if I can get things working via annotations, and then try to integrate API-notes later to see if I can simplify the whole setup.

I tried using the _Nonnull and _Nullable annotations and with both options I get the function imported as:

public class func Open(_ options: rocksdb.Options, _ name: std.string, _ dbptr: rocksdb.DB?) -> rocksdb.Status

I also tried the __attribute__((cf_returns_not_retained)) and __attribute__((cf_returns_retained)). Even though these are intended to work with Core Foundation, I was wondering if they could help here but it looks like they don't, as adding them does not make any changes to the original signature.
This is what I tried (with all s/cf/ns and s/not_/ combinations):

  static Status Open(const Options& options, const std::string& name,
                     DB ** dbptr __attribute__((cf_returns_not_retained)));

Any other ideas or anything else I might be missing?

Thanks!

I don't think you'd get "inout", you would get Unsafe[Mutable][Raw]Pointer instead.

  • Could you use "Open" with NULL as a last parameter?
  • Could you use "DB* db = NULL; Open(... &db);" as the last parameter?
  • Can Open modify DB? "const DB* db = ...; Open(..., &db)"
  • Can Open modify a DB pointer? "DB* const db = ...; Open(..., &db)"

Depending upon those answers annotate the pointers appropriately.

Open(..., C DB * CN * CN);

Here each C stands for either "const" or empty and each N stands for either _Nullable or _Nonnull (that's 32 possible combinations in total).

Yep, I don't mind, unsafe pointers would work as well but I'm not getting them either, no matter how I annotate the type. I'm wondering if it's a bug / limitation of the C++ interop, that's why I created this thread.

What the function does is to modify the object pointed to by dbptr. This is the implementation.
I guess the correct annotation would be:

Open(..., DB* _Nullable * const _Nonnull);

but this does not make any difference, in this case the function is imported like this:

func Open(..., _: rocksdb.DB?) -> rocksdb.Status

Try a simple example to see what's going on. This one compiles fine for me:

// .h
class DB;
DB* getDB(void);
void openDB(DB**);

// .cpp
class DB {};
DB* getDB() {
    return new DB();
}
void openDB(DB**) {}

// .swift
var db = getDB()
openDB(&db)

Interesting, thanks for checking the trivial case. I think it is because I'm using SWIFT_UNSAFE_REFERENCE in that class, as I need the type to be imported as a reference type.

#include <swift/bridging>

class DB {
public:
   void openDB(DB**); 
} SWIFT_UNSAFE_REFERENCE;

is imported as:

@available(macOS 13.3.0, *)
public class DB {   
    public func openDB(_: DB?!)
}

Is this expected / known? Should I open a bug?

Thanks!

I opened a bug about this: C++ interop: Pointer to pointer imported as `?!` in Swift when using SWIFT_UNSAFE_REFERENCE · Issue #70818 · apple/swift · GitHub