Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions bazel/bitdrift_swift_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,47 @@ def bitdrift_mobile_objc_test(name, srcs, data = [], deps = [], tags = [], visib
tags = tags,
visibility = visibility,
)

def bitdrift_mobile_swift_objc_test(name, srcs_swift, srcs_objc, hdrs_objc, data = [], deps = [], tags = [], use_test_host = False, repository = "", visibility = []):
test_swift_lib_name = name + "_swift_lib"
test_objc_lib_name = name + "_objc_lib"
swift_library(
name = test_swift_lib_name,
srcs = srcs_swift,
data = data,
deps = deps,
linkopts = ["-lresolv.9"],
testonly = True,
visibility = ["//visibility:private"],
tags = ["manual"],
)

objc_library(
name = test_objc_lib_name,
srcs = srcs_objc,
hdrs = hdrs_objc,
data = data,
deps = deps,
linkopts = ["-lresolv.9"],
testonly = True,
visibility = ["//visibility:private"],
tags = ["manual"],
)

test_host = None
if use_test_host:
test_host = "//test/platform/swift/test_host:TestHost"

ios_unit_test(
name = name,
data = data,
deps = [test_swift_lib_name, test_objc_lib_name],
minimum_os_version = MINIMUM_IOS_VERSION_TESTS,
timeout = "long",
tags = tags + [
"no-cache",
"no-remote",
],
test_host = test_host,
visibility = visibility,
)
2 changes: 2 additions & 0 deletions platform/swift/source/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ bitdrift_rust_library(
objc_library(
name = "objc_bridge",
srcs = [
"InternalAPI.m",
"ObjCWrapper.mm",
"reports/BitdriftKSCrashWrapper.m",
"reports/DiagnosticEventReporter.m",
],
hdrs = [
"CaptureRustBridge.h",
"InternalAPI.h",
"ObjCWrapper.h",
"reports/BitdriftKSCrashWrapper.h",
"reports/DiagnosticEventReporter.h",
Expand Down
133 changes: 133 additions & 0 deletions platform/swift/source/InternalAPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

/// Mechanism for exposing internal APIs to other libraries without exposing them publicly.
///
/// Place your exposable APIs in the `API Implementation` section at the bottom of the .m file.
/// They DON'T need to be declared in this header (although you can if you want to call them locally).
///
/// Be sure to version all APIs so that their behaviors can be safely modified in future,
/// and also proxy any returned non-standard-library objects so that they are safe to change in future.
///
/// ## How to use it (from a client)
///
/// Clients call `exposeAPI` to attach an internal (versioned) API method to the selector of their choice. e.g.
/// ```
/// NSError *error = [self.instance exposeAPI:@"example_v1" asSelector:@selector(example)];
/// ```
/// This code will add a new method with the selector `example` that actually calls the implementation of `example_v1` (if found).
/// See section `Implementing a client` for how to set up a client-library class to do this.
///
/// Note: In the following situations, calling the exposed method will no-op:
/// * The host API class is not found (e.g. this library hasn't been linked in)
/// * The requested API is not found (maybe it was misspelled, or has been removed)
///
///
/// ## Cloning this internal API hosting code for use in another library
///
/// Copy all code in the .m file except for these sections, which must be tailored to your implementation:
/// * `Library Specific`
/// * `API Implementation`
/// The rest is implementation-agnostic code.
///
///
/// ## Implementing a client
///
/// A client class (in the calling library) would look like this:
///
/// **ExampleClient.h**
///
/// ```objc
/// #import <Foundation/Foundation.h>
///
/// NS_ASSUME_NONNULL_BEGIN
///
/// @interface ExampleClient : NSObject
///
/// + (instancetype _Nullable)instance;
///
/// #pragma mark APIs we will expose from the host library
///
/// - (NSString *)example;
///
/// @end
///
/// NS_ASSUME_NONNULL_END
/// ```
///
/// **ExampleClient.m:**
///
/// ```objc
/// #import "ExampleClient.h"
///
/// #pragma clang diagnostic push
/// #pragma clang diagnostic ignored "-Wincomplete-implementation"
/// @implementation ExampleClient
///
/// #pragma mark Host library and APIs to expose
///
/// static NSString *hostLibraryAPIClassName = @"BD_InternalAPI_Capture";
///
/// static void exposeAPIs(id self) {
/// // Example of exposing APIs on initialize
/// NSError *error = [self exposeAPI:@"example_v1" asSelector:@selector(example)];
/// if(error != nil) {
/// // This would only happen if the ObjC runtime is very broken.
/// NSLog(@"API \"%@\" is unsafe to call", NSStringFromSelector(@selector(example)));
/// }
/// }
///
///
/// #pragma mark General code - Do not change
///
/// static id instance = nil;
///
/// + (void)initialize {
/// Class cls = NSClassFromString(hostLibraryAPIClassName);
/// static dispatch_once_t once;
/// dispatch_once(&once, ^{
/// instance = [cls instance];
/// if(instance != nil) {
/// exposeAPIs(self.instance);
/// } else {
/// NSLog(@"WARNING: API class %@ was not found. Calls to this API will no-op.", hostLibraryAPIClassName);
/// instance = [[self alloc] init];
/// }
/// });
/// }
///
/// + (instancetype _Nullable)instance {
/// return instance;
/// }
///
/// - (void)forwardInvocation:(NSInvocation *)invocation {
/// // If API calls will be made often, probably best to not log here.
/// NSLog(@"WARNING: API class %@ was not found. Called selector '%@' is a no-op.", hostLibraryAPIClassName, /// NSStringFromSelector(invocation.selector));
/// }
///
/// -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
/// // We must return a real signature or else it will crash, so use NSObject.init because it always exists.
/// return [NSObject instanceMethodSignatureForSelector:@selector(init)];
/// }
///
/// @end
/// ```

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface BD_InternalAPI_Capture : NSObject

+ (instancetype)instance;

// If you need to inject data into this internal API, create a `configure` method to do so:
// - (void)configureWithFribblefrabble:(Fribblefrabble *)frib;

@end

NS_ASSUME_NONNULL_END
181 changes: 181 additions & 0 deletions platform/swift/source/InternalAPI.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

#import "InternalAPI.h"
#import <objc/runtime.h>


#pragma mark Library Specific

// Place library-specific things (declarations, imports, etc) here.
// Library-specific API implementations are at the end of this file.
// Everything else in between should not be touched.

#define LIBRARY_NAME Capture


#pragma mark Base Implementation

#define CONCAT(a, ...) CONCAT2(a, __VA_ARGS__)
#define CONCAT2(a, ...) a ## __VA_ARGS__
#define STRINGIFY(s) STRINGIFY2(s)
#define STRINGIFY2(s) #s

#define API_CLASS CONCAT(BD_InternalAPI_, LIBRARY_NAME)
#define PROXY_CLASS CONCAT(BD_Proxy_, LIBRARY_NAME)
#define LIB_DOMAIN @"io.bitdrift." STRINGIFY(LIBRARY_NAME)


/**
* Proxy that no-ops whenever an unimplemented method is called.
*/
@interface PROXY_CLASS : NSProxy

@property(nonatomic,strong) id proxied;

@end

@implementation PROXY_CLASS

+ (instancetype)proxyTo:(id)objectToProxy {
if (objectToProxy == nil) {
return nil;
}

PROXY_CLASS *proxy = [PROXY_CLASS alloc];
proxy.proxied = objectToProxy;
return proxy;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
if ([self.proxied respondsToSelector:invocation.selector]) {
[invocation setTarget:self.proxied];
[invocation invoke];
}
}

-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
NSMethodSignature *signature = [self.proxied methodSignatureForSelector:selector];
if (signature != nil) {
return signature;
}

// We must return a real signature or else it will crash, so use NSObject.init because it always exists.
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

- (BOOL)isEqual:(id)object { return [self.proxied isEqual:object]; }
- (NSUInteger)hash { return [self.proxied hash]; }
- (Class)superclass { return [self.proxied superclass]; }
- (Class)class { return [self.proxied class]; }
- (BOOL)isProxy { return YES; }
- (BOOL)isKindOfClass:(Class)aClass { return [self.proxied isKindOfClass:aClass]; }
- (BOOL)isMemberOfClass:(Class)aClass { return [self.proxied isMemberOfClass:aClass]; }
- (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [self.proxied conformsToProtocol:aProtocol]; }
- (BOOL)respondsToSelector:(SEL)aSelector { return [self.proxied respondsToSelector:aSelector]; }
- (NSString *)description { return [self.proxied description]; }
- (NSString *)debugDescription { return [self.proxied debugDescription]; }

@end


@implementation API_CLASS

+ (instancetype)instance {
static API_CLASS *instance;
static dispatch_once_t once;
dispatch_once(&once, ^{ instance = [[self alloc] init]; });
return instance;
}

/**
* "null" method that gets mapped if a client requests an API name that doesn't exist.
*/
- (id)nullMethod {
return nil;
}

/**
* Expose an API on this class, assigning it to the specified selector so that it can be called normally.
* If the API name is not found, `nullMethod` will be mapped to the selector.
*
* @param apiName A string describing the selector of the internal method to map.
* @param asSelector The selector to map this method to (if found)
* @return `nil` on success, or an error if the objc runtime is seriously broken (almost impossible).
*/
- (NSError *)exposeAPI:(NSString * _Nonnull)apiName asSelector:(SEL)asSelector {
if(class_getInstanceMethod(self.class, asSelector) != nil) {
NSLog(@"WARNING: Class %@ already implements selector '%@'. Keeping existing mapping.", self.class, NSStringFromSelector(asSelector));
return nil;
}

SEL selectorToClone = @selector(nullMethod);

SEL foundSelector = NSSelectorFromString(apiName);
if(class_getInstanceMethod(self.class, foundSelector) != nil) {
selectorToClone = foundSelector;
} else {
NSLog(@"WARNING: Class %@ doesn't have method '%@' to clone. Mapping to a null implementation.", self.class, apiName);
}

#define NSERROR(FMT, ...) [NSError errorWithDomain:LIB_DOMAIN code:0 userInfo:@{ \
NSLocalizedDescriptionKey:[NSString stringWithFormat:FMT, __VA_ARGS__] }];

// Note: These errors should never happen unless the objective-c runtime is seriously broken.

Method method = class_getInstanceMethod(self.class, selectorToClone);
if(method == nil) {
return NSERROR(@"class_getInstanceMethod(%@, %@) failed", self.class, NSStringFromSelector(selectorToClone));
}

IMP implementation = method_getImplementation(method);
if(implementation == nil) {
return NSERROR(@"method_getImplementation(%@) failed", NSStringFromSelector(selectorToClone));
}

const char *encoding = method_getTypeEncoding(method);
if(encoding == nil) {
return NSERROR(@"method_getTypeEncoding(%@) failed", NSStringFromSelector(selectorToClone));
}

if(!class_addMethod(self.class, asSelector, implementation, encoding)) {
return NSERROR(@"class_addMethod(%@, %@, ...) failed", self.class, NSStringFromSelector(asSelector));
}

return nil;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
// If a particular API call will be made often, probably best to not log here.
NSLog(@"WARNING: Called nonexistent API '%@', which will no-op", NSStringFromSelector(invocation.selector));
}

-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
// We must return a real signature or else it will crash, so use NSObject.init because it always exists.
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}


#pragma mark API implementation


#ifdef DEBUG
// These are called by the unit tests. Do not remove them!

- (void)exampleWithVoidReturn_v1 {
}

- (NSString *)exampleWithIDReturn_v1 {
return @"This is an example API";
}

- (NSString *)exampleWithProxyReturn_v1 {
return (NSString *)[PROXY_CLASS proxyTo:@"This is a proxied string"];
}
#endif

@end
8 changes: 5 additions & 3 deletions test/platform/swift/unit_integration/core/BUILD
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_test")
load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_objc_test")

bitdrift_mobile_swift_test(
bitdrift_mobile_swift_objc_test(
name = "test",
srcs = glob(["**/*.swift"]) + ["//proto:report_swift_source"],
data = [
"testdata/lastCrash.bjn",
"testdata/metrickit-example.json",
],
repository = "@capture",
hdrs_objc = glob(["**/*.h"]),
srcs_objc = glob(["**/*.m"]),
srcs_swift = glob(["**/*.swift"]) + ["//proto:report_swift_source"],
tags = ["macos_only"],
visibility = ["//visibility:public"],
deps = [
Expand Down
Loading
Loading