Interfacing Swift with dynamically loaded OpenBLAS/LAPACKE symbols and complex pointer arguments

I'm developing a small package that wraps OpenBLAS (and LAPACKE therein) for my linear algebra library, since there doesn't seem to be such a package available that works for both Windows and Linux (on macOS I use Accelerate). After pondering for a while on how to make this package most distributable (for my fellow researchers), I decided to load the OpenBLAS symbols at runtime, using the following approach:

#if os(Windows)
import WinSDK
#elseif os(Linux)
import Linux
#endif

enum OpenBLAS {
    #if os(Windows)
    static let handle: HMODULE? = {
        if let handle = LoadLibraryA("libopenblas.dll") {
            return handle
        }
        print("Failed to load libopenblas.dll")
        return nil
    }()
    #elseif os(Linux)
    static let handle: UnsafeMutableRawPointer? = {
        if let handle = dlopen("libopenblas.so", RTLD_NOW | RTLD_LOCAL) {
            return handle
        }
        print("Failed to load libopenblas.so")
        return nil
    }()
    #endif
    
    static func load<T>(_ name: String, as type: T.Type = T.self) -> T? {
        guard let handle = handle else { return nil }
        #if os(Windows)
        let symbol = GetProcAddress(handle, name)
        #elseif os(Linux)
        let symbol = dlsym(handle, name)
        #endif
        if let symbol = symbol { return unsafeBitCast(symbol, to: type) }
        return nil
    }

    static let dscal: CBLAS_DSCAL = load("cblas_dscal")
    ...
}

typealias CBLAS_DSCAL = @convention(c) (_ N: Int32, _ alpha: Double, _ X: UnsafeMutablePointer<Double>?, _ incX: Int32) -> Void
...

Design note: The symbols are returned as optionals to support the case where OpenBLAS is not present on the user's system. In such cases, we fall back to naive implementations of the BLAS/LAPACK functions, etc.


So far, this has been a successful—albeit somewhat cumbersome—approach. The "problem" arises when I want to interface with LAPACKE functions that take complex inputs. The CBLAS functions accept complex values as Unsafe*RawPointer, which is very straightforward to interface with. However, LAPACKE expects complex inputs as pointers to structs like lapack_complex_double which are defined differently depending on the platform.

For example, the function LAPACKE_dgesv has the following signature:

lapack_int LAPACKE_dgesv(int matrix_layout, lapack_int n, lapack_int nrhs,
                         lapack_complex_double* a, lapack_int lda,
                         lapack_int* ipiv, lapack_complex_double* b,
                         lapack_int ldb );

On Windows, it's imported as:

func LAPACKE_dgesv(_ matrix_layout: Int32, _ n: Int32, _ nrhs: Int32, 
                   _ a: UnsafeMutablePointer<_Dcomplex>?, _ lda: Int32, 
                   _ ipiv: UnsafeMutablePointer<Int32>?, _ b: UnsafeMutablePointer<_Dcomplex>?, 
                   _ ldb: Int32) -> Int32

Here, the type _Dcomplex is essentially just a pair of Double values.


So, my question is: can I "safely" load, for example, LAPACKE_dgesv using the following signature?

typealias LAPACKE_DGESV = @convention(c) (_ matrix_layout: Int32, _ n: Int32, _ nrhs: Int32, 
                                          _ a: UnsafeMutableRawPointer?, _ lda: Int32, 
                                          _ ipiv: UnsafeMutablePointer<Int32>?, _ b: UnsafeMutableRawPointer?, 
                                          _ ldb: Int32) -> Int32

That is, can I safely switch from UnsafeMutablePointer<_Dcomplex> to UnsafeMutableRawPointer or OpaquePointer when loading these symbols?

I'll preface this by saying that the following package was built for a narrower audience than general use and was not built having Windows in mind, in fact I never tested it on Windows.

Having said that, GitHub - brokenhandsio/accelerate-linux: Accelerate APIs on Linux exists, a package somewhat similar to yours which aims to port the Accelerate API to Linux, and does so by exporting Accelerate on macOS and by wrapping OpenBLAS and LAPACKE on Linux. When possible, i.e. when Accelerate APIs match BLAS/LAPACK ones they are simply wrapped in a @_silgen_name as you can see here with the exact function you're having trouble with. In my opinion, bundled with some typealiases this works pretty well! As I said before I never tried it with Windows and therefore didn't encounter the type conversion issue, however I still think there's something useful here that we can recycle for your situation.
So, to finally answer your question:

  • personally I would go for a similar approach to the accelerate-linux package: typealias my way through with some #if os expressions, e.g.
    #if os(Windows)
    typealias LAComplexDouble = _Dcomplex
    #elseif os(Linux)
    typealias LAComplexDouble = lapack_complex_double
    #endif
    
    and then just use UnsafeMutablePointer<LAComplexDouble>?.
  • If you prefer your initial approach, I think it should work, the Raw in UnsafeRawPointer solves this exact question: accessing memory directly without a particular representation. I think you could get into trouble if you pass a type into the buffer which has a different memory layout, but you'll probably spot that when testing anyway.
1 Like

OpaquePointer is fairly miserable to work with, as you have likely found.

All of these struct layouts are ABI-identical on all of the platforms in question (they have the same size and alignment), and also match the layout of C's double[2] or _Complex double or Swift Numerics' Complex<Double>, so you can type-pun between these for the purposes of importing functions without introducing novel problems.

1 Like

When researching existing packages, I stumbled across yours and initially thought it would be perfect for the Linux part of my interface. I figured I could come up with a separate solution for Windows. However, the supercomputer environment I also need to support doesn't play well with system libraries—i.e., .systemLibrary(...)—since the platform doesn't natively support Swift. I have to jump through some hoops to make it work.

On Windows, linking with OpenBLAS at build time requires the corresponding import library. But since there isn’t really a standard way of installing OpenBLAS that, say, 90% of Windows users (or novice programmers) would be familiar with, other than either building it yourself or downloading dlls from the GitHub releases, SwiftPM can’t find the import library without using unsafe flags in the package manifest. That, in turn, means I wouldn’t be able to use the package as a dependency. The only viable option for me seems to be loading the symbols at runtime, as I described. That approach also happens to work in the supercomputer environment.

Of course, I could tell users of the library to pass the necessary linker flags to their swift build command, but that’s not really an option for me. As part of my "Swift advertising campaign", I’m trying to convince my coworkers—theoretical physicists with basic Python knowledge who aren’t particularly familiar with compilers, linkers, and such—about the potential of using Swift for the numerical work we do. So, I wanted to support the case where the user hasn’t installed OpenBLAS yet. And I need the package to be cross-platform and dead simple to use: all they should have to do is type swift run, and their program should run without any issues. If OpenBLAS/LAPACKE isn’t installed, no problem—we can install it together later. For now, the library will fall back to basic implementations of the matrix and vector operations.

The typealias approach seems to be the most correct. Using UnsafeRawPointer just makes it so simple to call them from Swift. I can basically just pass [Complex<Double>] as the parameter and that’s it.

2 Likes

Excellent. Indeed, I'm utilizing Swift Numerics for this. Specifically, I define Matrix<T> and Vector<T> types, for which I provide default implementations of basic linear algebra operations when T == AlgebraicField. Then, for cases where T == Double, T == Float, T == Complex<Double>, or T == Complex<Float>, I use the BLAS implementations if they are available. If not, the code falls back to the default implementations.

2 Likes