Hi Cory,
I think we're dealing with two separate issues here.
1) that all forward declared struct pointers get imported as an OpaquePointer
which makes us lose all type-safety
2) that it's a fairly frequent case that C libraries evolve from 'pointers to
fully declared structs' to 'pointers to forward declared structs'
Regarding 1)
------------
I fully agree that is pretty bad and I believe I have an idea what the Swift
importer should do for that case:
For the following C types
struct foo_s;
typedef foo_s *foo;
struct bar_s;
typedef bar_s *bar;
the C compiler today imports both `foo` and `bar` as `OpaquePointer` which
isn't very helpful. Instead I believe the importer should declare to phantom
types
enum foo_s {}
enum bar_s {}
and import `foo` as `typealias foo = OpaquePointer<foo_s>` and `bar` as
`typealias bar = OpaquePointer<bar_s>`.
That seems to preserve the right semantics:
- we can't have any values of `foo_s` or `bar_s` as they're phantom
- we can however have pointers to them
- in this example the pointers are `OpaquePointer<foo_s>` and
`OpaquePointer<bar_s>`, deliberately not `UnsafePointer<foo_s>` and
`UnsafePointer<bar_s>` so we don't have an issue that the user can do
'unsafePtr.pointee'
How do people think about this proposed change?
Regarding 2)
------------
I feel you'd prefer if we'd import the above as `typealias foo =
UnsafePointer<foo_s>` which I would be happy with but I can also see why
`OpaquePointer` exists: It stops us from dereferencing the pointer at compile
time (just like C does).
So yes, I agree we should fix (1). For the time being I believe I have
something that might work for you use-case even today (and would work even
nicer if the above changes are implemented):
Today, we already have the following initialisers
struct UnsafePointer<T> {
init?(_ ptr: OpaquePointer)
}
and
struct OpaquePointer {
init?<T>(_ ptr: UnsafePointer<T>)
}
by just adding the following two trivial ones:
extension UnsafePointer<T> {
init?(_ ptr: UnsafePointer<T>) { return ptr }
}
extension OpaquePointer {
init?<T>(_ ptr: OpaquePointer) { return ptr }
}
this should solve your problem:
- when you receive a pointer from the C library, store it as OpaquePointer
let myOpaquePointer: OpaquePointer =
OpaquePointer(c_library_create_function())
which now should work regardless of whether you're linking the old or the new
version of the C library
- when you pass a pointer to the C library:
c_library_consuming_function(.init(myOpaquePointer))
which should (modulo it doesn't right now
https://bugs.swift.org/browse/SR-6211) select the right initialiser for you,
only it doesn't 😲
But fortunately we can work around it:
--- SNIP ---
extension UnsafePointer {
static func make(_ ptr: UnsafePointer<Pointee>) -> UnsafePointer<Pointee> {
return ptr
}
static func make(_ ptr: OpaquePointer) -> UnsafePointer<Pointee> {
return UnsafePointer(ptr)
}
}
extension OpaquePointer {
static func make<T>(_ ptr: UnsafePointer<T>) -> OpaquePointer {
return OpaquePointer(ptr)!
}
static func make(_ ptr: OpaquePointer) -> OpaquePointer {
return ptr
}
}
func mockCLibraryCreateOld() -> UnsafePointer<Int> {
return UnsafePointer(UnsafeMutablePointer<Int>.allocate(capacity: 1))
}
func mockCLibraryCreateNew() -> OpaquePointer {
return OpaquePointer(mockCLibraryCreateOld())
}
func mockCLibraryConsumeOld(_ x: UnsafePointer<Int>) {}
func mockCLibraryConsumeNew(_ x: OpaquePointer) {}
let fromCold: OpaquePointer = .make(mockCLibraryCreateOld())
let fromCnew: OpaquePointer = .make(mockCLibraryCreateNew())
mockCLibraryConsumeOld(.make(fromCold))
mockCLibraryConsumeNew(.make(fromCnew))
mockCLibraryConsumeOld(.make(fromCnew))
mockCLibraryConsumeNew(.make(fromCold))
--- SNAP ---
HTH
-- Johannes
> On 24 Oct 2017, at 9:14 am, Cory Benfield via swift-evolution
> <[email protected]> wrote:
>
> I wanted to discuss a recent difficulty I’ve encountered while writing a
> Swift program that uses a C library that has recently changed its API to use
> opaque pointers, with an eye towards asking whether there are suggestions for
> ways to tackle the problem that I haven’t considered, or whether some
> enhancement to Swift should be proposed to provide a solution.
>
> A common trend in modern C code is to encapsulate application data by using
> pointers to “opaque” data structures: that is, data structures whose complete
> definition is not available in the header files for the library. This has
> many benefits from the perspective of library developers, mostly notably
> because it limits the ABI of the library, making it easier to change the
> internals without requiring recompilation or breaking changes. Pointers to
> these structures are translated into Swift code in the form of the
> OpaquePointer type.
>
> Older C libraries frequently have non-opaque structures: that is, the
> structure definition is available in the header files for the library. When
> using code like this from Swift, pointers to these structures are translated
> to Unsafe[Mutable]Pointer<T>.
>
> Both of these cases are well-handled by Swift today: opaque pointers
> correctly can do absolutely nothing, whereas typed pointers have the option
> of having behaviour based on knowing about the size of the data structure to
> which they point. All very good.
>
> A problem arises if a C dependency chooses to transition from non-opaque to
> opaque structures. This is a transition that well-maintained C libraries are
> strongly incentivised to make, but if you want to write Swift code that will
> compile against both the old and new version of the library you run into
> substantial issues. To illustrate the issue I’ll construct a small problem
> based on the most widely-used library to recently make this transition,
> OpenSSL.
>
> In OpenSSL 1.1.0 almost all of the previously-open data structures were made
> opaque, including the heavily used SSL_CTX structure. In terms of C code, the
> header file declaration changed from
>
>
> struct ssl_ctx_st {
> const SSL_METHOD *method;
> // snip 250 lines of structure declaration
> }
> typedef struct ssl_ctx_st SSL_CTX;
>
> to
>
> typedef struct ssl_ctx_st SSL_CTX;
>
>
> At an API level, any function that worked on the SSL_CTX structure that
> existed before this change was unaffected. For example, the function
> SSL_CTX_use_certificate has the same C API in both versions:
>
> int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x);
>
> Unfortunately, in Swift the API for this function changes dramatically, from
>
> func SSL_CTX_use_certificate(_ ctx: UnsafeMutablePointer<SSL_CTX>!,
> _ x:
> UnsafeMutablePointer<X509>!) -> Int32
>
> to
>
> func SSL_CTX_use_certificate(_ ctx: OpaquePointer!,
> _ x: OpaquePointer!)
> -> Int32
>
> The reason this is problematic is that there is no implicit cast in either
> direction between UnsafeMutablePointer<T> and OpaquePointer. This means the
> API here has changed in an incompatible way: types that are valid before the
> structure was made opaque are not valid afterwards. This adds a pretty
> substantial burden to supporting multiple versions of the same library from
> Swift code.
>
> So far I have thought of the following solutions to this problem that I can
> implement today:
>
> 1. Write a C wrapper library that exposes a third, consistent type that is
> the same on all versions. Most likely this would be done by re-exposing all
> these methods with arguments that take `void *` and performing the cast in C
> code. This, unfortunately, loses some of the Swift compiler’s ability to
> enforce type safety, as all these arguments will now be UnsafeRawPointer.
> This is not any worse than OpaquePointer, but it’s objectively worse than the
> un-opaqued version.
>
> 2. Write a C wrapper library that embeds these pointers in single, non-opaque
> structures with separate types. This allows us to keep the type safety at the
> cost of verbosity and an additional layer of indirection.
>
> 3. Write two different Swift wrappers for each of these versions that expose
> the same outer types, and transform them internally. Not ideal: the
> conditional compilation story here isn’t good and distributing this library
> via Swift PM would be hard.
>
> I’d be really interested in hearing whether there is a solution I’m missing
> that can be implemented today. If there is *not* such a solution, is there
> interest in attempting to tackle this problem in Swift more directly? There
> are plenty of language changes that could be made to solve this solution
> (e.g. changing OpaquePointer to OpaquePointer<T> but making it impossible to
> dereference, then treating UnsafePointer<T> as a subclass of
> OpaquePointer<T>, or making OpaquePointer a protocol implemented by
> UnsafePointer<T>, or all kinds of other things), but I wanted to hear from
> the community about suggested approach.
>
> I’d love not to have to manually maintain a C wrapper just to escape Swift’s
> type system here.
>
> Thanks,
>
> Cory
> _______________________________________________
> swift-evolution mailing list
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution