bclee
(Byoungchan Lee)
February 4, 2023, 4:16am
1
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.
#if !os(WASI)
import Dispatch
import Foundation
private enum ErrorResult {
case exception(NSException)
case error(Error)
case none
}
/// Only classes, protocols, methods, properties, and subscript declarations can be
/// bridges to Objective-C via the @objc keyword. This class encapsulates callback-style
/// asynchronous waiting logic so that it may be called from Objective-C and Swift.
internal class NMBWait: NSObject {
// About these kind of lines, `@objc` attributes are only required for Objective-C
// support, so that should be conditional on Darwin platforms and normal Xcode builds
// (non-SwiftPM builds).
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc
This file has been truncated. show original
Objective-C code in the same module then calls that class method.
}
NIMBLE_EXPORT NMBWaitUntilTimeoutBlock NMB_waitUntilTimeoutBuilder(NSString *file, NSUInteger line) {
return ^(NSTimeInterval timeout, void (^ _Nonnull action)(void (^ _Nonnull)(void))) {
[NMBWait untilTimeout:timeout file:file line:line action:action];
};
}
NIMBLE_EXPORT NMBWaitUntilBlock NMB_waitUntilBuilder(NSString *file, NSUInteger line) {
return ^(void (^ _Nonnull action)(void (^ _Nonnull)(void))) {
[NMBWait untilFile:file line:line action:action];
};
}
NS_ASSUME_NONNULL_END
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:
A module (static library with module-map file) is having mixed code (Objective-C + Swift) in an iOS App codebase. The Swift code is being used from module only within a the Objective C code. Ideally the swift interface need not to be public but internal. We can't as the documentation here says
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 prop…
Hi
We're building a binary framework and using Objective-C as the main language in the codebase with some files there written in Swift.
In order for us to expose our Swift classes to Objective-C, is by marking them public or open. As it's described here in Apple's documentation blog .
Now, that also exposes our Swift classes to the client too, and what we want is to expose them only to our framework target. Is there a way for us to do that?
Note: I quoted the following from Apple's docs.
Im…
opened 07:08PM - 14 Jun 17 UTC
compiler
feature
PrintAsObjC
objective-c interop
| | |
|------------------|-----------------|…
|Previous ID | SR-5221 |
|Radar | rdar://problem/59333073 |
|Original Reporter | @belkadan |
|Type | New Feature |
<details>
<summary>Additional Detail from JIRA</summary>
| | |
|------------------|-----------------|
|Votes | 1 |
|Component/s | Compiler |
|Labels | New Feature, PrintAsObjC |
|Assignee | @nkcsgexi |
|Priority | Medium |
md5: 9ba074083669e26a8ed45ebe1759e489
</details>
**Issue Description:**
To improve mixed Swift/Objective-C frameworks, there should be a way to generate a header for the `internal` parts of a Swift framework that are also `@objc`. This would act like a "project" header in Xcode that does not get copied into the build products.
This hasn't been implemented in the past mainly because it requires splitting the internal members of a public class out to a category, but support for "delaying" members got added last year in [PR 4155](https://github.com/apple/swift/pull/4155) anyway. This would just be another form of that.
Implementing this would involve something like the following:
- A new `-emit-objc-internal-header-path` command-line option.
- Rather than skipping over `internal` declarations in PrintAsObjC, adding them to a list much like the `delayedMembers` list today.
- Print all those declarations to a separate buffer, with a separate imports list.
- Write all that to a new header that imports `<MyModule/MyModule-Swift.h>`.
There are then Xcode-side changes that need to happen to make this a fully-supported feature, but this would be good enough to use it with a toolchain!
tera
February 4, 2023, 11:55am
2
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];
}
bclee
(Byoungchan Lee)
February 5, 2023, 6:21am
3
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.
}
static std::string getModuleContentsCxxString(ModuleDecl &M) {
SmallPtrSet<ImportModuleTy, 8> imports;
std::string moduleContentsBuf;
llvm::raw_string_ostream moduleContents{moduleContentsBuf};
printModuleContentsAsCxx(moduleContents, imports, M);
return moduleContents.str();
}
bool swift::printAsClangHeader(raw_ostream &os, ModuleDecl *M,
StringRef bridgingHeader,
bool ExposePublicDeclsInClangHeader) {
llvm::PrettyStackTraceString trace("While generating Clang header");
SmallPtrSet<ImportModuleTy, 8> imports;
std::string objcModuleContentsBuf;
llvm::raw_string_ostream objcModuleContents{objcModuleContentsBuf};
printModuleContentsAsObjC(objcModuleContents, imports, *M);
writePrologue(os, M->getASTContext(), computeMacroGuard(M));
emitObjCConditional(os,
In swift::printModuleContentsAsObjC
, the access level is determined by ModuleDecl::isExternallyConsumed
.
}
}
};
} // end anonymous namespace
static AccessLevel getRequiredAccess(const ModuleDecl &M) {
return M.isExternallyConsumed() ? AccessLevel::Public : AccessLevel::Internal;
}
void
swift::printModuleContentsAsObjC(raw_ostream &os,
llvm::SmallPtrSetImpl<ImportModuleTy> &imports,
ModuleDecl &M) {
llvm::raw_null_ostream prologueOS;
ModuleWriter(os, prologueOS, imports, M, getRequiredAccess(M),
OutputLanguageMode::ObjC)
.write();
}
void swift::printModuleContentsAsCxx(
raw_ostream &os, llvm::SmallPtrSetImpl<ImportModuleTy> &imports,
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.
if (hasEntryPoint()) {
return false;
}
// If an implicit Objective-C header was needed to construct this module, it
// must be the product of a library target.
if (!getImplicitImportInfo().BridgingHeaderPath.empty()) {
return false;
}
// App extensions are special beasts because they build without entrypoints
// like library targets, but they behave like executable targets because
// their associated modules are not suitable for distribution.
if (getASTContext().LangOpts.EnableAppExtensionRestrictions) {
return false;
}
// FIXME: This is still a lousy approximation of whether the module file will
// be externally consumed.
return true;
}
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
jrose
(Jordan Rose)
July 26, 2023, 7:56pm
4
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!