Is it possible to access internal methods of swift framework from Objective-C?

From what I understand, this is not possible, at least at compile time.

Because the generated header is part of the framework’s public interface, only declarations marked with the public or open modifier appear in the generated header for a framework target. Methods and properties that are marked with the internal modifier and declared within a class that inherits from an Objective-C class are accessible to the Objective-C runtime. However, they’re inaccessible at compile time and don’t appear in the generated header for a framework target.

However, some projects seem to perform such access without compilation errors.

Nimble (GitHub - Quick/Nimble: A Matcher Framework for Swift and Objective-C) defined a swift class called NMBWait with internal visibility, and also created several class methods.

Objective-C code in the same module then calls that class method.

I checked out Nimble's source code and built it in Xcode, and it built without a problem.

The generated Nimble-Swift.h header has the following code.

SWIFT_CLASS("_TtC6Nimble7NMBWait")
@interface NMBWait : NSObject
+ (void)untilTimeout:(NSTimeInterval)timeout file:(NSString * _Nonnull)file line:(NSUInteger)line action:(void (^ _Nonnull)(void (^ _Nonnull)(void)))action;
+ (void)untilFile:(NSString * _Nonnull)file line:(NSUInteger)line action:(void (^ _Nonnull)(void (^ _Nonnull)(void)))action;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

So even though NMBWait is declared as an internal visibility, it is exposed in the header for Swift and can be accessed from Objective-C. Not sure if this behavior is ok or if the documentation is wrong.

Related discussions:

If it's marked "objc" it is visible from Obj-C even if private:

// Swift:
class SecretClass: NSObject {
    @objc private func fugazi() {
        print("Here you go")
    }
}

hohoho() // prints: Here you go

// Obj-c:

@interface NSObject(AllSecretsToBeRevealed)
- (void)fugazi;
@end

void hohoho(void) {
    Class secretClass = NSClassFromString(@"ModuleName.SecretClass");
    assert(secretClass != nil);
    id object = [secretClass new];
    [object fugazi];
}

I found the answer. If I set APPLICATION_EXTENSION_API_ONLY=YES (or pass -application-extension to swift), internal visibility is also exposed as Objective-C Header.

I checked the following code:

swift::printAsClangHeader calls swift::printModuleContentsAsObjC for generating Objective-C headers.

In swift::printModuleContentsAsObjC, the access level is determined by ModuleDecl::isExternallyConsumed.

static AccessLevel getRequiredAccess(const ModuleDecl &M) {
  return M.isExternallyConsumed() ? AccessLevel::Public : AccessLevel::Internal;
}

ModuleDecl::isExternallyConsumed has a condition on whether this module is an application extension.

In Nimble's Xcode project, APPLICATION_EXTENSION_API_ONLY is set for that reason. (Set `APPLICATION_EXTENSION_API_ONLY` to `YES` · Quick/Nimble@ed94b22 · GitHub)

1 Like

Belated, but I think this one is my fault: application extensions look more like library targets in Xcode than applications, and so to get them to behave more like applications I think I added this check, piggybacking on the existing flag so we could ship Swift 2 or whatever. Of course, this flag doesn’t mean “this module is an application extension”; it means “this module is compatible with application extensions”. It would be up to the folks still at Apple to untangle this mess if they wanted to!