Dlopen failed: cannot locate symbol

While writing a Swift wrapper around the Android NDK API, I faced an issue when using functions from a higher NDK version than the current target Android SDK.

For example, AMotionEvent_getActionButton requires API level 33. If you use it directly and run the code on an Android version prior to 33, it causes a dlopen failed: cannot locate symbol error.

My solution was to redefine these functions manually using dynamic loading:

public func AMotionEvent_getActionButton(_ event: AInputEvent?) -> CInt {
    guard let handle = dlopen("libandroid.so", RTLD_NOW) else {
        fatalError("unexpected result")
    }
    defer {
        dlclose(handle)
    }
    guard let symbol = dlsym(handle, "AMotionEvent_getActionButton") else {
        return MotionEventButton.primary.rawValue
    }
    typealias Function = @convention(c) (OpaquePointer?) -> CInt
    let method = unsafeBitCast(symbol, to: Function.self)
    return method(event)
}

This works, but it's quite verbose.
Are there any better solutions?
Is a workaround similar to #available on Apple platforms planned for future Swift updates?

I believe this is the expected direction and there’s already a pull request under review at https://github.com/swiftlang/swift/pull/84574

As it happens, @madsodgaard has implemented exactly that in https://github.com/swiftlang/swift/pull/84574 :

@available(Android 28, *)
func availableIn28() {
  if #available(Android 30, *) {
    // Call Android 30+ method
  } else {
    // Fallback
  } 
}

Unfortunately, it isn't "available" yet (hopefully Swift 6.3). So for the time being, your dlsym check is probably the best you can do in pure Swift. The alternative would be to make a C shim that uses __BIONIC_AVAILABILITY, but that would require adding a whole new module.

The wording Android 30 here is a little unfortunate. Android API level 30 corresponds to public-facing version number 11.0. The public-facing version number actually isn't even always a number (e.g. 12L, for the release between 12.0 (API 31) and 13.0 (API 33)). Has this been a consideration? (When writing Android code in Kotlin or Java, the spelling is (Build.VERSION.SDK_INT >= 30).)

Do you mean to say that you think we should be surfacing the public-facing version number in addition to (or instead of) the API level? Or just that referring to it as Android 30 is confusing because it implies we mean the marketing version when we really mean the API level?

The latter. I don't think using public-facing version numbers in the check would be prudent as not every one has a corresponding SDK level (e.g. Android 7.1.1 and 7.1.2 are both API 25).

How is this sort of pattern handled in C on Android? e.g. I want to use API in API level 30, how do I check if it's available?

I've never actually done it myself, but I think it is something like this:

int callSomeAndroidAPI(const char* arg, int length) {
    if (__builtin_available(android 30, *)) {
        // API is available!
        AImageDecoder* decoder;
        AImageDecoder_createFromData(arg, length, 0, &decoder);
        AImageDecoder_destroy(decoder);
        return 1;  // success
    } else {
        // fallback implementation for older Android versions…
        return 0; // not available
    }
}

EDIT: A better (and official) example can be found at: https://developer.android.com/ndk/guides/using-newer-apis#guarded_api_calls

If the official documentation uses API levels rather than marketing versions, IMHO Swift should do the same. Consistency here means developers can apply knowledge from those official docs directly to their Swift code.

An example from that link:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

Which maps directly to @available(Android 31, *) in Swift, yeah?

Mosdef. I don't thinking @bbrk24 was suggesting we should use marketing versions, but rather just pointing out that the "Android 30" phrasing suggests we might mean the marketing version rather than the API level. Perhaps "AndroidAPI 30" would be preferable.

But I agree, if the NDK uses __builtin_available(android 31, *), then Swift should just mirror it and use @available(Android 31, *). Developers are used to ignoring marketing versions (iOS "26" notwithstanding).

4 Likes