1 #include <jni.h>
2 #include <jvmti.h>
3 #include <android/log.h>
4 #include <string>
5
6 namespace compose_inspection {
7
logE(const char * fmt,...)8 void logE(const char *fmt, ...) {
9 va_list args;
10 va_start(args, fmt);
11 __android_log_vprint(ANDROID_LOG_ERROR, "ComposeLayoutInspector", fmt, args);
12 va_end(args);
13 }
14
CheckJvmtiError(jvmtiEnv * jvmti,jvmtiError err_num)15 bool CheckJvmtiError(jvmtiEnv *jvmti, jvmtiError err_num) {
16 if (err_num == JVMTI_ERROR_NONE) {
17 return false;
18 }
19
20 char *error = nullptr;
21 jvmti->GetErrorName(err_num, &error);
22 logE("JVMTI error: %d(%s)", err_num, error == nullptr ? "Unknown" : error);
23 if (error != nullptr) {
24 jvmti->Deallocate((unsigned char *) error);
25 }
26
27 return true;
28 }
29
SetAllCapabilities(jvmtiEnv * jvmti)30 void SetAllCapabilities(jvmtiEnv *jvmti) {
31 jvmtiCapabilities caps;
32 jvmtiError error;
33 error = jvmti->GetPotentialCapabilities(&caps);
34 CheckJvmtiError(jvmti, error);
35 error = jvmti->AddCapabilities(&caps);
36 CheckJvmtiError(jvmti, error);
37 }
38
CreateJvmtiEnv(JavaVM * vm)39 jvmtiEnv *CreateJvmtiEnv(JavaVM *vm) {
40 jvmtiEnv *jvmti_env;
41 jint jvmti_flag = JVMTI_VERSION_1_2;
42 jint result = vm->GetEnv((void **) &jvmti_env, jvmti_flag);
43 if (result != JNI_OK) {
44 return nullptr;
45 }
46
47 return jvmti_env;
48 }
49
50 static jclass location_class = nullptr;
51 static jmethodID location_class_constructor = nullptr;
52
create_lambda_location_result_fields(JNIEnv * env)53 bool create_lambda_location_result_fields(JNIEnv *env) {
54 static bool failure_creating_result = false;
55
56 if (failure_creating_result) {
57 return false;
58 }
59 if (location_class != nullptr) {
60 return true;
61 }
62 failure_creating_result = true; // In case the next lines throw exceptions...
63 jclass local_location_class =
64 env->FindClass("androidx/compose/ui/inspection/LambdaLocation");
65 location_class = (jclass) env->NewGlobalRef(local_location_class);
66 location_class_constructor =
67 env->GetMethodID(location_class, "<init>", "(Ljava/lang/String;II)V");
68 if (location_class == nullptr || location_class_constructor == nullptr) {
69 return false;
70 }
71 failure_creating_result = false;
72 return true;
73 }
74
75 /**
76 * Create a Jvmti environment.
77 * The env is created and kept in a static for the duration of the JVM lifetime.
78 * Check if we are able to get the can_get_line_numbers capability. If not just
79 * return nullptr now and next time this is called.
80 */
getJvmti(JNIEnv * env)81 jvmtiEnv *getJvmti(JNIEnv *env) {
82 static jvmtiEnv *jvmti_env = nullptr;
83 static bool can_get_line_numbers = true;
84 if (jvmti_env != nullptr || !can_get_line_numbers) {
85 return jvmti_env;
86 }
87 can_get_line_numbers = false;
88 JavaVM *vm;
89 int error = env->GetJavaVM(&vm);
90 if (error != 0) {
91 logE("Failed to get JavaVM instance for LayoutInspector with error "
92 "code: %d",
93 error);
94 return nullptr;
95 }
96 // Create a stand-alone jvmtiEnv to avoid any callback conflicts
97 // with other profilers' agents.
98 jvmti_env = CreateJvmtiEnv(vm);
99 if (jvmti_env == nullptr) {
100 logE("Failed to initialize JVMTI env for LayoutInspector");
101 } else {
102 SetAllCapabilities(jvmti_env);
103 jvmtiCapabilities capabilities;
104 if (!CheckJvmtiError(jvmti_env,
105 jvmti_env->GetCapabilities(&capabilities))) {
106 can_get_line_numbers = capabilities.can_get_line_numbers;
107 }
108 if (!can_get_line_numbers) {
109 logE("Failed to get the can_get_line_numbers capability for JVMTI");
110 jvmti_env = nullptr;
111 }
112 }
113 return jvmti_env;
114 }
115
116 /**
117 * A range of instruction offsets that is known to originate from an inlined function.
118 */
119 typedef struct {
120 jlocation start_location;
121 jlocation end_location;
122 } InlineRange;
123
124 #ifdef DEBUG_ANALYZE_METHOD
125
dumpMethod(int lineCount,jvmtiLineNumberEntry * lines,int variableCount,jvmtiLocalVariableEntry * variables,int rangeCount,InlineRange * ranges)126 void dumpMethod(
127 int lineCount, jvmtiLineNumberEntry *lines,
128 int variableCount, jvmtiLocalVariableEntry *variables,
129 int rangeCount, InlineRange *ranges
130 ) {
131 logE("%s", "Analyze Method Lines");
132
133 logE("Local Variable table count=%d", variableCount);
134 for (int i=0; i<variableCount; i++) {
135 jvmtiLocalVariableEntry *var = &variables[i];
136 logE(" %d: start=%lld, length=%d, name=%s, signature=%s, slot=%d",
137 i, var->start_location, var->length, var->name, var->signature, var->slot);
138 }
139
140 logE("Line Number table count=%d", lineCount);
141 for (int i=0; i<lineCount; i++) {
142 jvmtiLineNumberEntry *line = &lines[i];
143 logE(" %d: start=%lld, line_number=%d",
144 i, line->start_location, line->line_number);
145 }
146
147 logE("Inline Ranges count=%d", rangeCount);
148 for (int i=0; i<rangeCount; i++) {
149 InlineRange *range = &ranges[i];
150 logE(" %d: start=%lld, end=%lld",
151 i, range->start_location, range->end_location);
152 }
153 }
154 #endif
155
156 /**
157 * Compute the ranges of inlined instructions from the local variables of a method.
158 *
159 * The range_ptr must be freed with jvmtiEnv.Deallocate.
160 */
computeInlineRanges(jvmtiEnv * jvmti,int variableCount,jvmtiLocalVariableEntry * variables,int * rangeCount_ptr,InlineRange ** ranges_ptr)161 void computeInlineRanges(
162 jvmtiEnv *jvmti,
163 int variableCount,
164 jvmtiLocalVariableEntry *variables,
165 int *rangeCount_ptr,
166 InlineRange **ranges_ptr
167 ) {
168 int count = 0;
169 InlineRange *ranges = nullptr;
170 for (int i=0; i<variableCount; i++) {
171 jvmtiLocalVariableEntry *variable = &variables[i];
172 char* name = variable->name;
173 if (name != nullptr && strncmp("$i$f$", name, 5) == 0) {
174 if (ranges == nullptr) {
175 jvmti->Allocate(sizeof(InlineRange) * (variableCount-i), (unsigned char **)&ranges);
176 }
177 ranges[count].start_location = variable->start_location;
178 ranges[count].end_location = variable->start_location + variable->length;
179 count++;
180 }
181 }
182 *rangeCount_ptr = count;
183 *ranges_ptr = ranges;
184 }
185
186 /**
187 * Return true if a given line is from an inline function.
188 * @param line to investigate
189 * @param rangeCount is the number of known inline ranges
190 * @param ranges the known inline ranges
191 * @return true if the line offset is within one of the inline ranges
192 */
isInlined(jvmtiLineNumberEntry * line,int rangeCount,InlineRange * ranges)193 bool isInlined(
194 jvmtiLineNumberEntry *line,
195 int rangeCount,
196 InlineRange *ranges
197 ) {
198 for (int i=0; i<rangeCount; i++) {
199 InlineRange *range = &ranges[i];
200 if (range->start_location <= line->start_location &&
201 line->start_location < range->end_location) {
202 return true;
203 }
204 }
205 return false;
206 }
207
208 /**
209 * Analyze the lines of a method to find start and end line excluding inlined functions.
210 * @param jvmti the JVMTI environment
211 * @param lineCount number of lines found in the method
212 * @param lines the actual lines sorted by line_number
213 * @param variableCount the number of entries of the local variables
214 * @param variables the actual variables
215 * @param start_line_ptr on return will hold the start line of this method or 0 if not found
216 * @param end_line_ptr on return will hold the end line of this method or 0 if not found
217 * @return true if a method range is found
218 */
analyzeLines(jvmtiEnv * jvmti,int lineCount,jvmtiLineNumberEntry * lines,int variableCount,jvmtiLocalVariableEntry * variables,int * start_line_ptr,int * end_line_ptr)219 bool analyzeLines(
220 jvmtiEnv *jvmti,
221 int lineCount, jvmtiLineNumberEntry *lines,
222 int variableCount, jvmtiLocalVariableEntry *variables,
223 int *start_line_ptr,
224 int *end_line_ptr
225 ) {
226 int rangeCount = 0;
227 InlineRange *ranges = nullptr;
228 computeInlineRanges(jvmti, variableCount, variables, &rangeCount, &ranges);
229 int start_line = 0;
230 int end_line = 0;
231
232 #ifdef DEBUG_ANALYZE_METHOD
233 dumpMethod(lineCount, lines, variableCount, variables, rangeCount, ranges);
234 #endif
235
236 // First find the start_line (ignore inlined information here):
237 for (int i=0; i<lineCount; i++) {
238 jvmtiLineNumberEntry *line = &lines[i];
239 int line_number = line->line_number;
240 if (line_number != 0) {
241 start_line = line_number;
242 break;
243 }
244 }
245
246 // Then find the end_line:
247 if (start_line > 0) {
248
249 // Some line numbers may appear multiple times in the line table.
250 // The algorithm below will check all the locations for a given line number.
251 // If any of the locations for a given line is inside an inline range, the line is excluded.
252
253 int prev_line = INT_MAX;
254 bool prev_inlined = true;
255 for (int i=lineCount - 1; i>=0; i--) {
256 jvmtiLineNumberEntry *line = &lines[i];
257 int line_number = line->line_number;
258 if (line_number == 0) {
259 continue;
260 }
261 if (prev_line > line_number) {
262 if (!prev_inlined) {
263 break;
264 } else {
265 prev_line = line_number;
266 prev_inlined = false;
267 }
268 }
269 prev_inlined |= isInlined(line, rangeCount, ranges);
270 }
271 end_line = !prev_inlined ? prev_line : start_line;
272 }
273 jvmti->Deallocate((unsigned char *)ranges);
274 *start_line_ptr = start_line;
275 *end_line_ptr = end_line;
276 return start_line > 0;
277 }
278
279 /**
280 * Deallocate the local variables and any allocations held by a local variable entry
281 * @param jvmti the JVMTI environment
282 * @param variableCount the number of entries of the local variables
283 * @param variables_ptr a reference to the actual variables
284 */
deallocateVariables(jvmtiEnv * jvmti,int variableCount,jvmtiLocalVariableEntry ** variables_ptr)285 void deallocateVariables(
286 jvmtiEnv *jvmti,
287 int variableCount,
288 jvmtiLocalVariableEntry **variables_ptr
289 ) {
290 jvmtiLocalVariableEntry *variables = *variables_ptr;
291 if (variables != nullptr) {
292 for (int i=0; i<variableCount; i++) {
293 jvmtiLocalVariableEntry *entry = &variables[i];
294 jvmti->Deallocate((unsigned char *)entry->name);
295 jvmti->Deallocate((unsigned char *)entry->signature);
296 jvmti->Deallocate((unsigned char *)entry->generic_signature);
297 }
298 jvmti->Deallocate((unsigned char *)variables);
299 }
300 *variables_ptr = nullptr;
301 }
302
deallocateLines(jvmtiEnv * jvmti,jvmtiLineNumberEntry ** lines_ptr)303 void deallocateLines(jvmtiEnv *jvmti, jvmtiLineNumberEntry **lines_ptr) {
304 jvmti->Deallocate((unsigned char *)*lines_ptr);
305 *lines_ptr = nullptr;
306 }
307
compareLineNumberEntry(const void * a,const void * b)308 int compareLineNumberEntry(const void *a, const void * b) {
309 return ((jvmtiLineNumberEntry*)a)->line_number - ((jvmtiLineNumberEntry*)b)->line_number;
310 }
311
312 const int ACC_BRIDGE = 0x40;
313
resolveLocation(JNIEnv * env,jclass lambda_class)314 jobject resolveLocation(JNIEnv *env, jclass lambda_class) {
315 if (!create_lambda_location_result_fields(env)) {
316 return nullptr;
317 }
318 jvmtiEnv *jvmti = getJvmti(env);
319 if (jvmti == nullptr) {
320 return nullptr;
321 }
322 int methodCount;
323 jmethodID *methods;
324 jvmtiError error = jvmti->GetClassMethods(lambda_class, &methodCount, &methods);
325 if (CheckJvmtiError(jvmti, error)) {
326 return nullptr;
327 }
328
329 int variableCount = 0;
330 jvmtiLocalVariableEntry *variables = nullptr;
331 int lineCount = 0;
332 jvmtiLineNumberEntry *lines = nullptr;
333 int start_line = 0;
334 int end_line = 0;
335
336 for (int i = 0; i < methodCount; i++) {
337 deallocateLines(jvmti, &lines);
338 deallocateVariables(jvmti, variableCount, &variables);
339
340 jmethodID methodId = methods[i];
341 int modifiers = 0;
342 error = jvmti->GetMethodModifiers(methodId, &modifiers);
343 if (CheckJvmtiError(jvmti, error)) {
344 break;
345 }
346 if ((modifiers & ACC_BRIDGE) != 0) { // NOLINT(hicpp-signed-bitwise)
347 continue; // Ignore bridge methods
348 }
349
350 char* name;
351 error = jvmti->GetMethodName(methodId, &name, nullptr, nullptr);
352 if (CheckJvmtiError(jvmti, error)) {
353 break;
354 }
355 bool isInvokeMethod = strcmp(name, "invoke") == 0;
356 bool isInvokeWithInlineParameter = strncmp(name, "invoke-", 7) == 0;
357 jvmti->Deallocate((unsigned char *)name);
358 if (!isInvokeMethod && !isInvokeWithInlineParameter) {
359 continue; // Ignore if the method name doesn't match "invoke"
360 }
361
362 error = jvmti->GetLocalVariableTable(methodId, &variableCount, &variables);
363 if (CheckJvmtiError(jvmti, error)) {
364 break;
365 }
366 error = jvmti->GetLineNumberTable(methodId, &lineCount, &lines);
367 if (CheckJvmtiError(jvmti, error)) {
368 break;
369 }
370 qsort(lines, lineCount, sizeof(jvmtiLineNumberEntry), compareLineNumberEntry);
371
372 if (analyzeLines(jvmti, lineCount, lines, variableCount, variables,
373 &start_line, &end_line)) {
374 break;
375 }
376 }
377 deallocateLines(jvmti, &lines);
378 deallocateVariables(jvmti, variableCount, &variables);
379 jvmti->Deallocate((unsigned char *)methods);
380
381 if (start_line <= 0) {
382 return nullptr;
383 }
384
385 char *source_name_ptr;
386 error = jvmti->GetSourceFileName(lambda_class, &source_name_ptr);
387 if (CheckJvmtiError(jvmti, error)) {
388 return nullptr;
389 }
390 jstring file_name = env->NewStringUTF(source_name_ptr);
391 jvmti->Deallocate((unsigned char *) source_name_ptr);
392 jobject result = env->NewObject(location_class, location_class_constructor,
393 file_name, start_line, end_line);
394 return result;
395 }
396 } // namespace compose_inspection
397
398 extern "C" {
399
400 JNIEXPORT jobject JNICALL
Java_androidx_compose_ui_inspection_LambdaLocation_resolve(JNIEnv * env,__unused jclass clazz,jclass lambda_class)401 Java_androidx_compose_ui_inspection_LambdaLocation_resolve(
402 JNIEnv *env, __unused jclass clazz, jclass lambda_class) {
403 return compose_inspection::resolveLocation(env, lambda_class);
404 }
405
406 }