This repository has no description
1// Redirects MLKit's NSBundle URLForResource:withExtension: lookups for its
2// resource bundles (MLKitPoseDetectionAccurateResources,
3// MLKitPoseDetectionCommonResources, MLKitXenoResources) to a directory the
4// library's Kotlin init code populates at runtime. Without this, MLKit's
5// `.tflite` / `.binarypb` weights have to be copied into the consumer's app
6// bundle via an Xcode build phase — forcing per-app setup.
7//
8// Kotlin side calls mlkit_set_resource_dir() with a Caches-directory path
9// after extracting the resource files there. The swizzled URLForResource
10// method first defers to the original (so non-MLKit lookups are unaffected),
11// then falls back to the registered directory for the specific bundle names
12// MLKit queries.
13
14#import <Foundation/Foundation.h>
15#import <objc/runtime.h>
16
17static NSString *_mlkitResourceDir = nil;
18
19// Install MLKit telemetry stubs. Safe to call multiple times.
20// Runs at two points: during +load (MLKit classes may not be registered yet,
21// in which case we silently skip and retry), and from mlkit_set_resource_dir
22// (called by Kotlin after app init — all classes definitely loaded by then).
23// Each stub has its own "installed" flag so partial success on the first
24// attempt still leaves work for the second.
25static void _pd_mlkit_noop_log(id, SEL, id, id);
26static void _pd_mlkit_noop_writelog(id, SEL, id, id, id, id, id, id, id);
27static id _pd_mlkit_compute_url(Class, SEL, id, id, id);
28static void _pd_mlkit_noop_start_auto_upload(id, SEL);
29static void _pd_mlkit_noop_flush_upload(id, SEL, id, BOOL);
30static void _pd_mlkit_noop_log_counters(id, SEL, id);
31
32static BOOL _pd_logger_stubbed = NO;
33static BOOL _pd_writer_stubbed = NO;
34static BOOL _pd_fileutil_stubbed = NO;
35static BOOL _pd_uploader_start_stubbed = NO;
36static BOOL _pd_uploader_flush_stubbed = NO;
37static BOOL _pd_metalogger_stubbed = NO;
38
39static void _pd_install_clearcut_stub(void) {
40 // 1. -[MLKITx_CCTClearcutLogger log:completion:] (high-level entry)
41 if (!_pd_logger_stubbed) {
42 Class logger = NSClassFromString(@"MLKITx_CCTClearcutLogger");
43 if (logger) {
44 Method m = class_getInstanceMethod(logger,
45 @selector(log:completion:));
46 if (m) {
47 method_setImplementation(m, (IMP)_pd_mlkit_noop_log);
48 _pd_logger_stubbed = YES;
49 }
50 }
51 }
52 // 2. -[MLKITx_CCTLogWriter writeLog:pseudonymousID:logDirectory:clock:
53 // logTransformers:completionQueue:completion:]
54 // Some callers bypass the Logger and hit the LogWriter directly —
55 // which is the path kima's crash stack showed.
56 if (!_pd_writer_stubbed) {
57 Class writer = NSClassFromString(@"MLKITx_CCTLogWriter");
58 if (writer) {
59 Method m = class_getInstanceMethod(writer,
60 @selector(writeLog:pseudonymousID:logDirectory:clock:logTransformers:completionQueue:completion:));
61 if (m) {
62 method_setImplementation(m, (IMP)_pd_mlkit_noop_writelog);
63 _pd_writer_stubbed = YES;
64 }
65 }
66 }
67 // 3. Fallback: if neither stub fires and something reaches
68 // +[MLKITx_CCTClearcutFileUtility computeUrlForLogContextDir:context:
69 // bundleId:], make that class method respond instead of throwing.
70 if (!_pd_fileutil_stubbed) {
71 Class fileutil = NSClassFromString(@"MLKITx_CCTClearcutFileUtility");
72 if (fileutil) {
73 SEL sel = @selector(computeUrlForLogContextDir:context:bundleId:);
74 // Try to override the existing method impl if there IS one; if
75 // not (this is what causes the original crash — the class method
76 // table has lost the entry), add it via the metaclass.
77 Method existing = class_getClassMethod(fileutil, sel);
78 if (existing) {
79 method_setImplementation(existing, (IMP)_pd_mlkit_compute_url);
80 _pd_fileutil_stubbed = YES;
81 } else {
82 Class meta = object_getClass((id)fileutil);
83 if (meta && class_addMethod(meta, sel,
84 (IMP)_pd_mlkit_compute_url,
85 "@@:@@@")) {
86 _pd_fileutil_stubbed = YES;
87 }
88 }
89 }
90 }
91 // 4. Auto-upload path (separate from writeLog — fires on a periodic
92 // timer, not per pose detection). Stub at three levels so any call
93 // into the chain gets neutralized before reaching the broken
94 // logCounters / flushCounters code.
95 Class uploader = NSClassFromString(@"MLKITx_CCTClearcutUploader");
96 if (uploader) {
97 if (!_pd_uploader_start_stubbed) {
98 Method m = class_getInstanceMethod(uploader,
99 @selector(startAutoUpload));
100 if (m) {
101 method_setImplementation(m, (IMP)_pd_mlkit_noop_start_auto_upload);
102 _pd_uploader_start_stubbed = YES;
103 }
104 }
105 if (!_pd_uploader_flush_stubbed) {
106 Method m = class_getInstanceMethod(uploader,
107 @selector(flushThenUploadWithCompletionHandler:isOnForeground:));
108 if (m) {
109 method_setImplementation(m, (IMP)_pd_mlkit_noop_flush_upload);
110 _pd_uploader_flush_stubbed = YES;
111 }
112 }
113 }
114 if (!_pd_metalogger_stubbed) {
115 Class meta = NSClassFromString(@"MLKITx_CCTClearcutMetaLogger");
116 if (meta) {
117 Method m = class_getInstanceMethod(meta,
118 @selector(logCounters:));
119 if (m) {
120 method_setImplementation(m, (IMP)_pd_mlkit_noop_log_counters);
121 _pd_metalogger_stubbed = YES;
122 }
123 }
124 }
125}
126
127__attribute__((visibility("default")))
128void mlkit_set_resource_dir(const char *path) {
129 if (path) {
130 _mlkitResourceDir = [NSString stringWithUTF8String:path];
131 } else {
132 _mlkitResourceDir = nil;
133 }
134 _pd_install_clearcut_stub();
135}
136
137static NSSet<NSString *> *mlkitBundleNames(void) {
138 static NSSet *names;
139 static dispatch_once_t once;
140 dispatch_once(&once, ^{
141 names = [NSSet setWithObjects:
142 @"MLKitPoseDetectionAccurateResources",
143 @"MLKitPoseDetectionCommonResources",
144 @"MLKitXenoResources",
145 nil];
146 });
147 return names;
148}
149
150// No-op replacement for -[MLKITx_CCTClearcutLogger log:completion:].
151// MLKit's Clearcut telemetry subsystem crashes at runtime on some iOS
152// versions when it tries to compute a log-context URL (the selector
153// `+[MLKITx_CCTClearcutFileUtility computeUrlForLogContextDir:context:bundleId:]`
154// goes missing at the app-link step even with -ObjC when the consumer
155// builds a static framework). Telemetry isn't needed for pose detection,
156// so we replace the top-level log entry with a completion-handler call
157// that signals success but does nothing.
158static void _pd_mlkit_noop_log(id self, SEL _cmd, id event, id completion) {
159 if (completion) {
160 void (^block)(BOOL, NSError *) = completion;
161 block(YES, nil);
162 }
163}
164
165// No-op replacement for the 7-arg LogWriter writeLog. Some callers bypass
166// CCTClearcutLogger and call CCTLogWriter directly — stubbing only the
167// Logger isn't enough. This signature matches the actual method:
168// -[MLKITx_CCTLogWriter writeLog:pseudonymousID:logDirectory:clock:
169// logTransformers:completionQueue:completion:]
170static void _pd_mlkit_noop_writelog(id self, SEL _cmd,
171 id log, id pid, id logDir, id clock,
172 id transformers, id completionQueue,
173 id completion) {
174 if (completion) {
175 void (^block)(BOOL, NSError *) = completion;
176 if (completionQueue) {
177 // Preserve semantics: callers typically want the completion
178 // delivered on their queue. dispatch_async into it, else call
179 // inline.
180 dispatch_async((dispatch_queue_t)completionQueue, ^{
181 block(YES, nil);
182 });
183 } else {
184 block(YES, nil);
185 }
186 }
187}
188
189// Fallback for the missing +[MLKITx_CCTClearcutFileUtility
190// computeUrlForLogContextDir:context:bundleId:] class method. Return the
191// logContextDir NSURL unchanged — good enough to satisfy any caller that
192// slips past our writeLog stub.
193static id _pd_mlkit_compute_url(Class self, SEL _cmd,
194 id logContextDir, id context, id bundleId) {
195 return logContextDir;
196}
197
198// MLKit also starts a periodic auto-upload timer that crashes when it
199// reaches CCTClearcutMetaLogger logCounters:. Separate code path from
200// writeLog. We stub it at multiple levels for belt-and-braces:
201// top: -[MLKITx_CCTClearcutUploader startAutoUpload]
202// — cleanest, prevents the timer from starting
203// middle: -[MLKITx_CCTClearcutUploader
204// flushThenUploadWithCompletionHandler:isOnForeground:]
205// — invoked directly when foreground-entering; no-op + call
206// completion
207// bottom: -[MLKITx_CCTClearcutMetaLogger logCounters:]
208// — crash site itself; fallback if upper stubs miss it
209static void _pd_mlkit_noop_start_auto_upload(id self, SEL _cmd) {
210 // Do nothing — auto-upload stays off.
211}
212
213static void _pd_mlkit_noop_flush_upload(id self, SEL _cmd,
214 id completion, BOOL isForeground) {
215 if (completion) {
216 void (^block)(BOOL) = completion;
217 block(YES);
218 }
219}
220
221static void _pd_mlkit_noop_log_counters(id self, SEL _cmd, id counters) {
222 // Do nothing — counters are silently dropped.
223}
224
225@interface NSBundle (PoseDetectionMLKitRedirect)
226@end
227
228@implementation NSBundle (PoseDetectionMLKitRedirect)
229
230+ (void)load {
231 static dispatch_once_t once;
232 dispatch_once(&once, ^{
233 Method origM = class_getInstanceMethod(self,
234 @selector(URLForResource:withExtension:));
235 Method newM = class_getInstanceMethod(self,
236 @selector(pd_mlkit_URLForResource:withExtension:));
237 method_exchangeImplementations(origM, newM);
238
239 // Try to install the Clearcut no-op now; if the class isn't
240 // registered yet (MLKit loaded after us), this is a silent no-op
241 // and mlkit_set_resource_dir re-attempts later.
242 _pd_install_clearcut_stub();
243 });
244}
245
246- (NSURL *)pd_mlkit_URLForResource:(NSString *)name
247 withExtension:(NSString *)ext {
248 // Swizzled — this now calls the ORIGINAL implementation.
249 NSURL *fromOriginal = [self pd_mlkit_URLForResource:name withExtension:ext];
250 if (fromOriginal) return fromOriginal;
251
252 if (!_mlkitResourceDir || !name || !ext) return nil;
253 if (![ext isEqualToString:@"bundle"]) return nil;
254 if (![mlkitBundleNames() containsObject:name]) return nil;
255
256 NSString *candidate = [_mlkitResourceDir stringByAppendingPathComponent:
257 [NSString stringWithFormat:@"%@.%@", name, ext]];
258 BOOL isDir = NO;
259 if ([[NSFileManager defaultManager] fileExistsAtPath:candidate isDirectory:&isDir] && isDir) {
260 return [NSURL fileURLWithPath:candidate isDirectory:YES];
261 }
262 return nil;
263}
264
265@end