Skip to content

Commit 7c36871

Browse files
committed
Trace native methods calls using FFM APIs
1 parent fa3ea25 commit 7c36871

17 files changed

Lines changed: 422 additions & 357 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package datadog.trace.bootstrap.instrumentation.ffm;
2+
3+
import datadog.context.ContextScope;
4+
import datadog.trace.api.InstrumenterConfig;
5+
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
6+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
7+
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
8+
import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator;
9+
import java.lang.invoke.MethodHandle;
10+
import java.lang.invoke.MethodHandles;
11+
import java.lang.invoke.MethodType;
12+
import java.util.Set;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
public final class FFMNativeMethodDecorator extends BaseDecorator {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(FFMNativeMethodDecorator.class);
18+
private static final CharSequence TRACE_FFM = UTF8BytesString.create("trace-ffm");
19+
private static final CharSequence OPERATION_NAME = UTF8BytesString.create("trace.native");
20+
21+
private static final MethodHandle START_SPAN_MH =
22+
safeFindStatic(
23+
"startSpan",
24+
MethodType.methodType(ContextScope.class, CharSequence.class, boolean.class));
25+
private static final MethodHandle END_SPAN_MH =
26+
safeFindStatic(
27+
"endSpan",
28+
MethodType.methodType(Object.class, Throwable.class, ContextScope.class, Object.class));
29+
30+
public static final FFMNativeMethodDecorator DECORATE = new FFMNativeMethodDecorator();
31+
32+
private static MethodHandle safeFindStatic(String name, MethodType methodType) {
33+
try {
34+
return MethodHandles.lookup().findStatic(FFMNativeMethodDecorator.class, name, methodType);
35+
} catch (Throwable t) {
36+
LOGGER.debug("Cannot find method {} in NativeMethodHandleWrapper", name, t);
37+
return null;
38+
}
39+
}
40+
41+
public static MethodHandle wrap(
42+
final MethodHandle original, final String libraryName, final String methodName) {
43+
if (START_SPAN_MH == null || END_SPAN_MH == null) {
44+
return original;
45+
}
46+
try {
47+
MethodType originalType = original.type();
48+
boolean isVoid = originalType.returnType() == void.class;
49+
50+
// We need the ContextScope to be visible to the finally block.
51+
// Easiest way is to artificially prepend it to the target signature.
52+
// The added parameter is ignored by the original handle.
53+
// originalWithScope: (ContextScope, args...) -> R
54+
MethodHandle originalWithScope = MethodHandles.dropArguments(original, 0, ContextScope.class);
55+
56+
/*
57+
* Build the cleanup handle used by MethodHandles.tryFinally.
58+
*
59+
* tryFinally has a strict calling convention:
60+
* - void target -> cleanup(Throwable, ContextScope, args...)
61+
* - non-void -> cleanup(Throwable, R, ContextScope, args...)
62+
*
63+
* END_SPAN_MH is (Throwable, ContextScope, Object) -> Object,
64+
* so we need to reshape it to match what tryFinally expects.
65+
*/
66+
MethodHandle cleanup;
67+
68+
if (isVoid) {
69+
// No return value: bind `null` as the result argument.
70+
MethodHandle endWithNull = MethodHandles.insertArguments(END_SPAN_MH, 2, (Object) null);
71+
72+
// Make it accept the original arguments even though they are unused.
73+
MethodHandle endDropped =
74+
MethodHandles.dropArguments(endWithNull, 2, originalType.parameterList());
75+
76+
// tryFinally requires void return for void targets.
77+
cleanup = endDropped.asType(endDropped.type().changeReturnType(void.class));
78+
79+
} else {
80+
/*
81+
* Non-void case:
82+
* tryFinally will call cleanup as:
83+
* (Throwable, returnValue, ContextScope, args...)
84+
*
85+
* END_SPAN_MH expects:
86+
* (Throwable, ContextScope, result)
87+
*
88+
* So we first permute parameters to swap returnValue and ContextScope.
89+
*/
90+
MethodHandle endPermuted =
91+
MethodHandles.permuteArguments(
92+
END_SPAN_MH,
93+
MethodType.methodType(
94+
Object.class, Throwable.class, Object.class, ContextScope.class),
95+
0,
96+
2,
97+
1);
98+
99+
// Accept original arguments (unused) after the required ones.
100+
MethodHandle endDropped =
101+
MethodHandles.dropArguments(endPermuted, 3, originalType.parameterList());
102+
103+
// Adapt return and result parameter types to match the original signature.
104+
MethodType cleanupType =
105+
endDropped
106+
.type()
107+
.changeParameterType(1, originalType.returnType())
108+
.changeReturnType(originalType.returnType());
109+
110+
cleanup = endDropped.asType(cleanupType);
111+
}
112+
113+
// Wrap the original in try/finally semantics.
114+
// Resulting handle:
115+
// (ContextScope, args...) -> R
116+
MethodHandle withFinally = MethodHandles.tryFinally(originalWithScope, cleanup);
117+
118+
// Precompute span metadata so we don't redo the lookup per invocation.
119+
final CharSequence resourceName = resourceNameFor(libraryName, methodName);
120+
final boolean methodMeasured = isMethodMeasured(libraryName, methodName);
121+
122+
// Bind both arguments to startSpan.
123+
// After binding: () -> ContextScope
124+
MethodHandle boundStart =
125+
MethodHandles.insertArguments(START_SPAN_MH, 0, resourceName, methodMeasured);
126+
127+
// Make it look like it takes the same arguments as the original,
128+
// even though they are ignored.
129+
// (args...) -> ContextScope
130+
MethodHandle startCombiner =
131+
MethodHandles.dropArguments(boundStart, 0, originalType.parameterList());
132+
133+
/*
134+
* foldArguments wires it all together:
135+
*
136+
* scope = startCombiner(args...)
137+
* return withFinally(scope, args...)
138+
*
139+
* Final shape matches the original:
140+
* (args...) -> R
141+
*/
142+
return MethodHandles.foldArguments(withFinally, startCombiner);
143+
144+
} catch (Throwable t) {
145+
LOGGER.debug(
146+
"Cannot wrap method handle for library {} and method {}", libraryName, methodName, t);
147+
return original;
148+
}
149+
}
150+
151+
@SuppressWarnings("unused")
152+
public static ContextScope startSpan(CharSequence resourceName, boolean methodMeasured) {
153+
AgentSpan span = AgentTracer.startSpan(TRACE_FFM.toString(), OPERATION_NAME);
154+
DECORATE.afterStart(span);
155+
span.setResourceName(resourceName);
156+
return AgentTracer.activateSpan(span);
157+
}
158+
159+
@SuppressWarnings("unused")
160+
public static Object endSpan(Throwable t, ContextScope scope, Object result) {
161+
try {
162+
if (scope != null) {
163+
final AgentSpan span = AgentSpan.fromContext(scope.context());
164+
scope.close();
165+
166+
if (span != null) {
167+
if (t != null) {
168+
DECORATE.onError(span, t);
169+
span.addThrowable(t);
170+
}
171+
172+
span.finish();
173+
}
174+
}
175+
} catch (Throwable ignored) {
176+
177+
}
178+
return result;
179+
}
180+
181+
public static boolean isMethodTraced(final String library, final String method) {
182+
return matches(InstrumenterConfig.get().getTraceNativeMethods().get(library), method);
183+
}
184+
185+
public static boolean isMethodMeasured(final String library, final String method) {
186+
return matches(InstrumenterConfig.get().getMeasureMethods().get(library), method);
187+
}
188+
189+
public static CharSequence resourceNameFor(final String library, final String method) {
190+
if (library == null || library.isEmpty()) {
191+
return UTF8BytesString.create(method);
192+
}
193+
return UTF8BytesString.create(library + "." + method);
194+
}
195+
196+
private static boolean matches(final Set<String> allows, final String method) {
197+
if (allows == null) {
198+
return false;
199+
}
200+
return allows.contains(method) || allows.contains("*");
201+
}
202+
203+
@Override
204+
protected String[] instrumentationNames() {
205+
return new String[] {TRACE_FFM.toString()};
206+
}
207+
208+
@Override
209+
protected CharSequence spanType() {
210+
return null;
211+
}
212+
213+
@Override
214+
protected CharSequence component() {
215+
return TRACE_FFM;
216+
}
217+
}

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextInjector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public ClassVisitor wrap(
106106
final MethodList<?> methods,
107107
final int writerFlags,
108108
final int readerFlags) {
109-
return new ClassVisitor(Opcodes.ASM8, classVisitor) {
109+
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
110110

111111
private final boolean frames =
112112
implementationContext.getClassFileVersion().isAtLeast(ClassFileVersion.JAVA_V6);

dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/context/FieldBackedContextRequestRewriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public ClassVisitor wrap(
8484
final MethodList<?> methods,
8585
final int writerFlags,
8686
final int readerFlags) {
87-
return new ClassVisitor(Opcodes.ASM8, classVisitor) {
87+
return new ClassVisitor(Opcodes.ASM9, classVisitor) {
8888
@Override
8989
public MethodVisitor visitMethod(
9090
final int access,

dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
1 io.opentelemetry.javaagent.*
4545
1 java.*
4646
0 java.lang.ClassLoader
47+
0 java.lang.foreign.*
48+
0 jdk.internal.foreign.*
4749
# allow exception profiling instrumentation
4850
0 java.lang.Exception
4951
0 java.lang.Error

dd-java-agent/instrumentation/java/java-lang/java-lang-22.0/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ tracerJava {
1616
}
1717

1818
testJvmConstraints {
19-
minJavaVersion = JavaVersion.VERSION_22
19+
minJavaVersion = JavaVersion.VERSION_25
2020
}
2121

2222
idea {

dd-java-agent/instrumentation/java/java-lang/java-lang-22.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk22/FFMApiModule.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import datadog.trace.agent.tooling.Instrumenter;
77
import datadog.trace.agent.tooling.InstrumenterModule;
88
import datadog.trace.api.InstrumenterConfig;
9+
import datadog.trace.api.Pair;
910
import java.util.HashMap;
1011
import java.util.List;
1112
import java.util.Map;
@@ -19,8 +20,8 @@ public FFMApiModule() {
1920
@Override
2021
public Map<String, String> contextStore() {
2122
final Map<String, String> ret = new HashMap<>();
22-
ret.put("java.lang.foreign.SymbolLookup", "java.lang.String");
23-
ret.put("java.lang.foreign.MemorySegment", "java.lang.CharSequence");
23+
ret.put("java.lang.foreign.SymbolLookup", String.class.getName());
24+
ret.put("java.lang.foreign.MemorySegment", Pair.class.getName());
2425
return ret;
2526
}
2627

@@ -33,12 +34,4 @@ public boolean isEnabled() {
3334
public List<Instrumenter> typeInstrumentations() {
3435
return asList(new LinkerInstrumentation(), new SymbolLookupInstrumentation());
3536
}
36-
37-
@Override
38-
public String[] helperClassNames() {
39-
return new String[] {
40-
// this could be moved to the boostrap eventually
41-
"datadog.trace.instrumentation.trace_annotation.TraceDecorator",
42-
};
43-
}
4437
}

dd-java-agent/instrumentation/java/java-lang/java-lang-22.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk22/LinkerInstrumentation.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package datadog.trace.instrumentation.java.lang.jdk22;
22

3-
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.hasInterface;
44
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
55
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
67

78
import datadog.trace.agent.tooling.Instrumenter;
89
import net.bytebuddy.description.type.TypeDescription;
@@ -19,13 +20,18 @@ public String hierarchyMarkerType() {
1920

2021
@Override
2122
public ElementMatcher<TypeDescription> hierarchyMatcher() {
22-
return implementsInterface(named("java.lang.foreign.Linker"));
23+
return hasInterface(named("java.lang.foreign.Linker"));
2324
}
2425

2526
@Override
2627
public void methodAdvice(MethodTransformer transformer) {
2728
transformer.applyAdvice(
28-
isMethod().and(named("downcallHandle")),
29+
isMethod().and(named("defaultLookup")),
30+
"datadog.trace.instrumentation.java.lang.jdk22.SymbolLookupAdvices$CaptureDefaultLookup");
31+
transformer.applyAdvice(
32+
isMethod()
33+
.and(named("downcallHandle"))
34+
.and(takesArgument(0, named("java.lang.foreign.MemorySegment"))),
2935
"datadog.trace.instrumentation.java.lang.jdk22.DownCallWrapAdvice");
3036
}
3137
}

dd-java-agent/instrumentation/java/java-lang/java-lang-22.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk22/SymbolLookupInstrumentation.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
88

99
import datadog.trace.agent.tooling.Instrumenter;
10-
import datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers;
1110
import net.bytebuddy.description.type.TypeDescription;
1211
import net.bytebuddy.matcher.ElementMatcher;
1312

@@ -30,9 +29,6 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() {
3029

3130
@Override
3231
public void methodAdvice(MethodTransformer transformer) {
33-
transformer.applyAdvice(
34-
isMethod().and(isStatic()).and(named("defaultLookup")),
35-
"datadog.trace.instrumentation.java.lang.jdk22.SymbolLookupAdvices$CaptureDefaultLookup");
3632
transformer.applyAdvice(
3733
isMethod()
3834
.and(isStatic())
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
package datadog.trace.instrumentation.java.lang.jdk22;
22

3+
import static datadog.trace.bootstrap.instrumentation.ffm.FFMNativeMethodDecorator.wrap;
34

5+
import datadog.trace.api.Pair;
46
import datadog.trace.bootstrap.InstrumentationContext;
5-
import net.bytebuddy.asm.Advice;
67
import java.lang.foreign.MemorySegment;
78
import java.lang.invoke.MethodHandle;
8-
import java.lang.invoke.MethodHandles;
9+
import net.bytebuddy.asm.Advice;
910

1011
public class DownCallWrapAdvice {
1112
@Advice.OnMethodExit(suppress = Throwable.class)
12-
public static void onExit(@Advice.Argument(0) final MemorySegment memorySegment, @Advice.Return(readOnly = false)MethodHandle handle) {
13-
if (memorySegment == null || !Boolean.TRUE.equals(InstrumentationContext.get(MemorySegment.class, Boolean.class).get(memorySegment))) {
13+
public static void onExit(
14+
@Advice.Argument(0) final MemorySegment memorySegment,
15+
@Advice.Return(readOnly = false) MethodHandle handle) {
16+
if (memorySegment == null) {
17+
return;
18+
}
19+
final Pair<String, String> libAndMethod =
20+
InstrumentationContext.get(MemorySegment.class, Pair.class).get(memorySegment);
21+
if (libAndMethod == null) {
1422
return;
1523
}
16-
MethodHandles.
24+
handle = wrap(handle, libAndMethod.getLeft(), libAndMethod.getRight());
1725
}
1826
}

0 commit comments

Comments
 (0)