Skip to content

Commit b43cbb3

Browse files
committed
This adds a mechanism for "publishing" internal APIs for use across our libraries.
A client library loads the API class "BD_InternalAPI_Capture" and then calls "exposeAPI" on it to link up at runtime. If the class fails to load, or the requested API doesn't exist, it downgrades gracefully to a no-op rather than crashing. See InternalAPI.h for a more thorough description + example client.
1 parent 4893a0a commit b43cbb3

File tree

10 files changed

+713
-3
lines changed

10 files changed

+713
-3
lines changed

bazel/bitdrift_swift_test.bzl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,47 @@ def bitdrift_mobile_objc_test(name, srcs, data = [], deps = [], tags = [], visib
5858
tags = tags,
5959
visibility = visibility,
6060
)
61+
62+
def bitdrift_mobile_swift_objc_test(name, srcs_swift, srcs_objc, hdrs_objc, data = [], deps = [], tags = [], use_test_host = False, repository = "", visibility = []):
63+
test_swift_lib_name = name + "_swift_lib"
64+
test_objc_lib_name = name + "_objc_lib"
65+
swift_library(
66+
name = test_swift_lib_name,
67+
srcs = srcs_swift,
68+
data = data,
69+
deps = deps,
70+
linkopts = ["-lresolv.9"],
71+
testonly = True,
72+
visibility = ["//visibility:private"],
73+
tags = ["manual"],
74+
)
75+
76+
objc_library(
77+
name = test_objc_lib_name,
78+
srcs = srcs_objc,
79+
hdrs = hdrs_objc,
80+
data = data,
81+
deps = deps,
82+
linkopts = ["-lresolv.9"],
83+
testonly = True,
84+
visibility = ["//visibility:private"],
85+
tags = ["manual"],
86+
)
87+
88+
test_host = None
89+
if use_test_host:
90+
test_host = "//test/platform/swift/test_host:TestHost"
91+
92+
ios_unit_test(
93+
name = name,
94+
data = data,
95+
deps = [test_swift_lib_name, test_objc_lib_name],
96+
minimum_os_version = MINIMUM_IOS_VERSION_TESTS,
97+
timeout = "long",
98+
tags = tags + [
99+
"no-cache",
100+
"no-remote",
101+
],
102+
test_host = test_host,
103+
visibility = visibility,
104+
)

platform/swift/source/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ bitdrift_rust_library(
8080
objc_library(
8181
name = "objc_bridge",
8282
srcs = [
83+
"InternalAPI.m",
8384
"ObjCWrapper.mm",
8485
"reports/BitdriftKSCrashWrapper.m",
8586
"reports/DiagnosticEventReporter.m",
8687
],
8788
hdrs = [
8889
"CaptureRustBridge.h",
90+
"InternalAPI.h",
8991
"ObjCWrapper.h",
9092
"reports/BitdriftKSCrashWrapper.h",
9193
"reports/DiagnosticEventReporter.h",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
/// Mechanism for exposing internal APIs to other libraries without exposing them publicly.
9+
///
10+
/// Place your exposable APIs in the `API Implementation` section at the bottom of the .m file.
11+
/// They DON'T need to be declared in this header (although you can if you want to call them locally).
12+
///
13+
/// Be sure to version all APIs so that their behaviors can be safely modified in future,
14+
/// and also proxy any returned non-standard-library objects so that they are safe to change in future.
15+
///
16+
/// ## How to use it (from a client)
17+
///
18+
/// Clients call `exposeAPI` to attach an internal (versioned) API method to the selector of their choice. e.g.
19+
/// ```
20+
/// NSError *error = [self.instance exposeAPI:@"example_v1" asSelector:@selector(example)];
21+
/// ```
22+
/// This code will add a new method with the selector `example` that actually calls the implementation of `example_v1` (if found).
23+
/// See section `Implementing a client` for how to set up a client-library class to do this.
24+
///
25+
/// Note: In the following situations, calling the exposed method will no-op:
26+
/// * The host API class is not found (e.g. this library hasn't been linked in)
27+
/// * The requested API is not found (maybe it was misspelled, or has been removed)
28+
///
29+
///
30+
/// ## Cloning this internal API hosting code for use in another library
31+
///
32+
/// Copy all code in the .m file except for these sections, which must be tailored to your implementation:
33+
/// * `Library Specific`
34+
/// * `API Implementation`
35+
/// The rest is implementation-agnostic code.
36+
///
37+
///
38+
/// ## Implementing a client
39+
///
40+
/// A client class (in the calling library) would look like this:
41+
///
42+
/// **ExampleClient.h**
43+
///
44+
/// ```objc
45+
/// #import <Foundation/Foundation.h>
46+
///
47+
/// NS_ASSUME_NONNULL_BEGIN
48+
///
49+
/// @interface ExampleClient : NSObject
50+
///
51+
/// + (instancetype _Nullable)instance;
52+
///
53+
/// #pragma mark APIs we will expose from the host library
54+
///
55+
/// - (NSString *)example;
56+
///
57+
/// @end
58+
///
59+
/// NS_ASSUME_NONNULL_END
60+
/// ```
61+
///
62+
/// **ExampleClient.m:**
63+
///
64+
/// ```objc
65+
/// #import "ExampleClient.h"
66+
///
67+
/// #pragma clang diagnostic push
68+
/// #pragma clang diagnostic ignored "-Wincomplete-implementation"
69+
/// @implementation ExampleClient
70+
///
71+
/// #pragma mark Host library and APIs to expose
72+
///
73+
/// static NSString *hostLibraryAPIClassName = @"BD_InternalAPI_Capture";
74+
///
75+
/// static void exposeAPIs(id self) {
76+
/// // Example of exposing APIs on initialize
77+
/// NSError *error = [self exposeAPI:@"example_v1" asSelector:@selector(example)];
78+
/// if(error != nil) {
79+
/// // This would only happen if the ObjC runtime is very broken.
80+
/// NSLog(@"API \"%@\" is unsafe to call", NSStringFromSelector(@selector(example)));
81+
/// }
82+
/// }
83+
///
84+
///
85+
/// #pragma mark General code - Do not change
86+
///
87+
/// static id instance = nil;
88+
///
89+
/// + (void)initialize {
90+
/// Class cls = NSClassFromString(hostLibraryAPIClassName);
91+
/// static dispatch_once_t once;
92+
/// dispatch_once(&once, ^{
93+
/// instance = [cls instance];
94+
/// if(instance != nil) {
95+
/// exposeAPIs(self.instance);
96+
/// } else {
97+
/// NSLog(@"WARNING: API class %@ was not found. Calls to this API will no-op.", hostLibraryAPIClassName);
98+
/// instance = [[self alloc] init];
99+
/// }
100+
/// });
101+
/// }
102+
///
103+
/// + (instancetype _Nullable)instance {
104+
/// return instance;
105+
/// }
106+
///
107+
/// - (void)forwardInvocation:(NSInvocation *)invocation {
108+
/// // If API calls will be made often, probably best to not log here.
109+
/// NSLog(@"WARNING: API class %@ was not found. Called selector '%@' is a no-op.", hostLibraryAPIClassName, /// NSStringFromSelector(invocation.selector));
110+
/// }
111+
///
112+
/// -(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
113+
/// // We must return a real signature or else it will crash, so use NSObject.init because it always exists.
114+
/// return [NSObject instanceMethodSignatureForSelector:@selector(init)];
115+
/// }
116+
///
117+
/// @end
118+
/// ```
119+
120+
#import <Foundation/Foundation.h>
121+
122+
NS_ASSUME_NONNULL_BEGIN
123+
124+
@interface BD_InternalAPI_Capture : NSObject
125+
126+
+ (instancetype)instance;
127+
128+
// If you need to inject data into this internal API, create a `configure` method to do so:
129+
// - (void)configureWithFribblefrabble:(Fribblefrabble *)frib;
130+
131+
@end
132+
133+
NS_ASSUME_NONNULL_END
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
#import "InternalAPI.h"
9+
#import <objc/runtime.h>
10+
11+
12+
#pragma mark Library Specific
13+
14+
// Place library-specific things (declarations, imports, etc) here.
15+
// Library-specific API implementations are at the end of this file.
16+
// Everything else in between should not be touched.
17+
18+
#define LIBRARY_NAME Capture
19+
20+
21+
#pragma mark Base Implementation
22+
23+
#define CONCAT(a, ...) CONCAT2(a, __VA_ARGS__)
24+
#define CONCAT2(a, ...) a ## __VA_ARGS__
25+
#define STRINGIFY(s) STRINGIFY2(s)
26+
#define STRINGIFY2(s) #s
27+
28+
#define API_CLASS CONCAT(BD_InternalAPI_, LIBRARY_NAME)
29+
#define PROXY_CLASS CONCAT(BD_Proxy_, LIBRARY_NAME)
30+
#define LIB_DOMAIN @"io.bitdrift." STRINGIFY(LIBRARY_NAME)
31+
32+
33+
/**
34+
* Proxy that no-ops whenever an unimplemented method is called.
35+
*/
36+
@interface PROXY_CLASS : NSProxy
37+
38+
@property(nonatomic,strong) id proxied;
39+
40+
@end
41+
42+
@implementation PROXY_CLASS
43+
44+
+ (instancetype)proxyTo:(id)objectToProxy {
45+
if (objectToProxy == nil) {
46+
return nil;
47+
}
48+
49+
PROXY_CLASS *proxy = [PROXY_CLASS alloc];
50+
proxy.proxied = objectToProxy;
51+
return proxy;
52+
}
53+
54+
- (void)forwardInvocation:(NSInvocation *)invocation {
55+
if ([self.proxied respondsToSelector:invocation.selector]) {
56+
[invocation setTarget:self.proxied];
57+
[invocation invoke];
58+
}
59+
}
60+
61+
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
62+
NSMethodSignature *signature = [self.proxied methodSignatureForSelector:selector];
63+
if (signature != nil) {
64+
return signature;
65+
}
66+
67+
// We must return a real signature or else it will crash, so use NSObject.init because it always exists.
68+
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
69+
}
70+
71+
- (BOOL)isEqual:(id)object { return [self.proxied isEqual:object]; }
72+
- (NSUInteger)hash { return [self.proxied hash]; }
73+
- (Class)superclass { return [self.proxied superclass]; }
74+
- (Class)class { return [self.proxied class]; }
75+
- (BOOL)isProxy { return YES; }
76+
- (BOOL)isKindOfClass:(Class)aClass { return [self.proxied isKindOfClass:aClass]; }
77+
- (BOOL)isMemberOfClass:(Class)aClass { return [self.proxied isMemberOfClass:aClass]; }
78+
- (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [self.proxied conformsToProtocol:aProtocol]; }
79+
- (BOOL)respondsToSelector:(SEL)aSelector { return [self.proxied respondsToSelector:aSelector]; }
80+
- (NSString *)description { return [self.proxied description]; }
81+
- (NSString *)debugDescription { return [self.proxied debugDescription]; }
82+
83+
@end
84+
85+
86+
@implementation API_CLASS
87+
88+
+ (instancetype)instance {
89+
static API_CLASS *instance;
90+
static dispatch_once_t once;
91+
dispatch_once(&once, ^{ instance = [[self alloc] init]; });
92+
return instance;
93+
}
94+
95+
/**
96+
* "null" method that gets mapped if a client requests an API name that doesn't exist.
97+
*/
98+
- (id)nullMethod {
99+
return nil;
100+
}
101+
102+
/**
103+
* Expose an API on this class, assigning it to the specified selector so that it can be called normally.
104+
* If the API name is not found, `nullMethod` will be mapped to the selector.
105+
*
106+
* @param apiName A string describing the selector of the internal method to map.
107+
* @param asSelector The selector to map this method to (if found)
108+
* @return `nil` on success, or an error if the objc runtime is seriously broken (almost impossible).
109+
*/
110+
- (NSError *)exposeAPI:(NSString * _Nonnull)apiName asSelector:(SEL)asSelector {
111+
if(class_getInstanceMethod(self.class, asSelector) != nil) {
112+
NSLog(@"WARNING: Class %@ already implements selector '%@'. Keeping existing mapping.", self.class, NSStringFromSelector(asSelector));
113+
return nil;
114+
}
115+
116+
SEL selectorToClone = @selector(nullMethod);
117+
118+
SEL foundSelector = NSSelectorFromString(apiName);
119+
if(class_getInstanceMethod(self.class, foundSelector) != nil) {
120+
selectorToClone = foundSelector;
121+
} else {
122+
NSLog(@"WARNING: Class %@ doesn't have method '%@' to clone. Mapping to a null implementation.", self.class, apiName);
123+
}
124+
125+
#define NSERROR(FMT, ...) [NSError errorWithDomain:LIB_DOMAIN code:0 userInfo:@{ \
126+
NSLocalizedDescriptionKey:[NSString stringWithFormat:FMT, __VA_ARGS__] }];
127+
128+
// Note: These errors should never happen unless the objective-c runtime is seriously broken.
129+
130+
Method method = class_getInstanceMethod(self.class, selectorToClone);
131+
if(method == nil) {
132+
return NSERROR(@"class_getInstanceMethod(%@, %@) failed", self.class, NSStringFromSelector(selectorToClone));
133+
}
134+
135+
IMP implementation = method_getImplementation(method);
136+
if(implementation == nil) {
137+
return NSERROR(@"method_getImplementation(%@) failed", NSStringFromSelector(selectorToClone));
138+
}
139+
140+
const char *encoding = method_getTypeEncoding(method);
141+
if(encoding == nil) {
142+
return NSERROR(@"method_getTypeEncoding(%@) failed", NSStringFromSelector(selectorToClone));
143+
}
144+
145+
if(!class_addMethod(self.class, asSelector, implementation, encoding)) {
146+
return NSERROR(@"class_addMethod(%@, %@, ...) failed", self.class, NSStringFromSelector(asSelector));
147+
}
148+
149+
return nil;
150+
}
151+
152+
- (void)forwardInvocation:(NSInvocation *)invocation {
153+
// If a particular API call will be made often, probably best to not log here.
154+
NSLog(@"WARNING: Called nonexistent API '%@', which will no-op", NSStringFromSelector(invocation.selector));
155+
}
156+
157+
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
158+
// We must return a real signature or else it will crash, so use NSObject.init because it always exists.
159+
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
160+
}
161+
162+
163+
#pragma mark API implementation
164+
165+
166+
#ifdef DEBUG
167+
// These are called by the unit tests. Do not remove them!
168+
169+
- (void)exampleWithVoidReturn_v1 {
170+
}
171+
172+
- (NSString *)exampleWithIDReturn_v1 {
173+
return @"This is an example API";
174+
}
175+
176+
- (NSString *)exampleWithProxyReturn_v1 {
177+
return (NSString *)[PROXY_CLASS proxyTo:@"This is a proxied string"];
178+
}
179+
#endif
180+
181+
@end

test/platform/swift/unit_integration/core/BUILD

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_test")
1+
load("//bazel:bitdrift_swift_test.bzl", "bitdrift_mobile_swift_objc_test")
22

3-
bitdrift_mobile_swift_test(
3+
bitdrift_mobile_swift_objc_test(
44
name = "test",
5-
srcs = glob(["**/*.swift"]) + ["//proto:report_swift_source"],
65
data = [
76
"testdata/lastCrash.bjn",
87
"testdata/metrickit-example.json",
98
],
109
repository = "@capture",
10+
hdrs_objc = glob(["**/*.h"]),
11+
srcs_objc = glob(["**/*.m"]),
12+
srcs_swift = glob(["**/*.swift"]) + ["//proto:report_swift_source"],
1113
tags = ["macos_only"],
1214
visibility = ["//visibility:public"],
1315
deps = [

0 commit comments

Comments
 (0)