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 }