Skip to content

Commit 87a02b4

Browse files
authored
Support generation of multiple types of glue per-module (#29)
1 parent 5d426dd commit 87a02b4

File tree

9 files changed

+194
-100
lines changed

9 files changed

+194
-100
lines changed

build.gradle.kts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ tasks.jar {
4848
tasks.javadoc {
4949
dependsOn(embed)
5050
setSource(allSources())
51-
exclude(
52-
"datadog/instrument/glue",
53-
"datadog/instrument/utils/Glue.java",
54-
"datadog/instrument/utils/JVM.java")
51+
exclude("datadog/instrument/glue", "datadog/instrument/utils/JVM.java")
5552
var javadocOptions = (options as StandardJavadocDocletOptions)
5653
if (JavaVersion.current().isJava9Compatible) {
5754
javadocOptions.addBooleanOption("html5", true)

buildSrc/src/main/kotlin/instrument-glue.gradle.kts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ dependencies {
2525
}
2626

2727
tasks.register<JavaExec>("generateGlue") {
28-
val glue: String by project.extra
28+
val glue: List<String> by project.extra
2929

3030
// generate glue files under a consistent packaging location
31-
val resourcesDir = generatedGlueResources.dir("datadog/instrument/glue/")
32-
val javaDir = generatedGlueJava.dir("datadog/instrument/glue/")
31+
val resourcePath = generatedGlueResources.dir("datadog/instrument/glue/")
32+
val javaPath = generatedGlueJava.dir("datadog/instrument/glue/")
3333

3434
group = "Build"
35-
description = "Generate ${glue}"
36-
mainClass = "datadog.instrument.glue.${glue}Generator"
35+
description = "Generate Instrumentation Glue"
36+
mainClass = "datadog.instrument.glue.GlueGenerator"
3737
classpath = sourceSets["glue"].runtimeClasspath
38-
args = listOf(resourcesDir.toString(), javaDir.toString())
39-
outputs.dirs(resourcesDir, javaDir)
38+
args = listOf(resourcePath.toString(), javaPath.toString()) + glue
39+
outputs.dirs(resourcePath, javaPath)
4040
}
4141

4242
tasks.processResources {

class-inject/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55

66
// class-inject generates glue bytecode at build-time to provide access to Unsafe.defineClass
77
// we inline this glue as a string constant, then install it at runtime using instrumentation
8-
extra["glue"] = "DefineClassGlue"
8+
extra["glue"] = listOf("DefineClassGlue")
99

1010
tasks.jar {
1111
// DefineClassGlue only contains large string constants that get inlined into ClassInjector

class-inject/src/glue/java/datadog/instrument/glue/DefineClassGlueGenerator.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66

77
package datadog.instrument.glue;
88

9-
import static datadog.instrument.utils.Glue.classHeader;
10-
import static datadog.instrument.utils.Glue.packBytecode;
9+
import static datadog.instrument.glue.GlueGenerator.classHeader;
10+
import static datadog.instrument.glue.GlueGenerator.packBytecode;
1111
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
1212
import static org.objectweb.asm.Opcodes.*;
1313

1414
import java.io.IOException;
1515
import java.nio.charset.StandardCharsets;
1616
import java.nio.file.Files;
1717
import java.nio.file.Path;
18-
import java.nio.file.Paths;
1918
import java.util.ArrayList;
2019
import java.util.List;
2120
import java.util.function.BiFunction;
@@ -351,11 +350,8 @@ private static byte[] generateBytecode(String unsafeNamespace) {
351350
return cw.toByteArray();
352351
}
353352

354-
public static void main(String[] args) throws IOException {
355-
if (args.length != 2) {
356-
throw new IllegalArgumentException("Expected: resources-dir java-dir");
357-
}
358-
Path defineClassGlue = Paths.get(args[1], "DefineClassGlue.java");
353+
public static void generateGlue(Path resourcePath, Path javaPath) throws IOException {
354+
Path defineClassGlue = javaPath.resolve("DefineClassGlue.java");
359355
List<String> lines = new ArrayList<>();
360356
classHeader(lines, "DefineClassGlue");
361357
lines.add(" /** Glue Id */");
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2025-Present Datadog, Inc.
5+
*/
6+
7+
package datadog.instrument.glue;
8+
9+
import static java.nio.charset.StandardCharsets.UTF_16BE;
10+
11+
import datadog.instrument.utils.JVM;
12+
import java.io.ByteArrayOutputStream;
13+
import java.io.InputStream;
14+
import java.util.MissingResourceException;
15+
import java.util.Objects;
16+
17+
/** Methods for loading instrumentation glue bytecode from string literals and resource files. */
18+
public final class Glue {
19+
20+
private static final String GLUE_RESOURCE_PREFIX = "/datadog/instrument/glue/";
21+
22+
private static final int BUFFER_SIZE = 8192;
23+
24+
private Glue() {}
25+
26+
/**
27+
* Unpacks a string literal produced by {@link GlueGenerator#packBytecode} back into bytecode.
28+
*
29+
* @param bytecode the packed bytecode
30+
* @return the unpacked bytecode
31+
*/
32+
public static byte[] unpackBytecode(String bytecode) {
33+
return bytecode.getBytes(UTF_16BE);
34+
}
35+
36+
/**
37+
* Loads glue bytecode with the given name from the given host.
38+
*
39+
* @param host the host class used to load the glue resource
40+
* @param glueName the glue resource containing the bytecode
41+
* @return the glue bytecode
42+
* @throws MissingResourceException if the bytecode cannot be read
43+
*/
44+
@SuppressWarnings({"Since15"})
45+
public static byte[] loadBytecode(Class<?> host, String glueName) {
46+
String glueResource = GLUE_RESOURCE_PREFIX + glueName;
47+
try (InputStream is = Objects.requireNonNull(host.getResourceAsStream(glueResource))) {
48+
if (JVM.atLeastJava(9)) {
49+
return is.readAllBytes();
50+
} else {
51+
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
52+
int bytesRead;
53+
byte[] buf = new byte[BUFFER_SIZE];
54+
while ((bytesRead = is.read(buf, 0, BUFFER_SIZE)) != -1) {
55+
os.write(buf, 0, bytesRead);
56+
}
57+
return os.toByteArray();
58+
}
59+
}
60+
} catch (Throwable e) {
61+
String detail = "Cannot load " + glueResource + " from " + host + ": " + e;
62+
throw new MissingResourceException(detail, host.getName(), glueResource);
63+
}
64+
}
65+
}

utils/src/main/java/datadog/instrument/utils/Glue.java renamed to utils/src/main/java/datadog/instrument/glue/GlueGenerator.java

Lines changed: 21 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,31 @@
44
* Copyright 2025-Present Datadog, Inc.
55
*/
66

7-
package datadog.instrument.utils;
7+
package datadog.instrument.glue;
88

9-
import static java.nio.charset.StandardCharsets.UTF_16BE;
10-
11-
import java.io.ByteArrayOutputStream;
12-
import java.io.InputStream;
9+
import java.lang.reflect.Method;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
1312
import java.util.List;
14-
import java.util.MissingResourceException;
15-
import java.util.Objects;
1613

17-
/** Methods for packing glue bytecode into strings that can be stored in the constant pool. */
18-
public final class Glue {
14+
/** Generates instrumentation glue for projects that apply the {@code instrument-glue} plugin. */
15+
public final class GlueGenerator {
16+
17+
private GlueGenerator() {}
1918

20-
private Glue() {}
19+
/** Entry-point for generating instrumentation glue into resource files or Java source code. */
20+
public static void main(String[] args) throws ReflectiveOperationException {
21+
if (args.length < 3) {
22+
throw new IllegalArgumentException("Expected: resource-path java-path glue-name...");
23+
}
24+
Path resourcePath = Paths.get(args[0]);
25+
Path javaPath = Paths.get(args[1]);
26+
for (int i = 2; i < args.length; i++) {
27+
Class<?> generatorClass = Class.forName("datadog.instrument.glue." + args[i] + "Generator");
28+
Method generateGlue = generatorClass.getMethod("generateGlue", Path.class, Path.class);
29+
generateGlue.invoke(null, resourcePath, javaPath);
30+
}
31+
}
2132

2233
/**
2334
* Writes the source header of a class to hold the string representation of glue bytecode.
@@ -63,48 +74,4 @@ public static void packBytecode(List<String> lines, byte[] bytecode) {
6374
}
6475
lines.add(buf + "\";");
6576
}
66-
67-
/**
68-
* Unpacks a string literal produced by {@link #packBytecode} back into the original bytecode.
69-
*
70-
* @param bytecode the packed bytecode
71-
* @return the unpacked bytecode
72-
*/
73-
public static byte[] unpackBytecode(String bytecode) {
74-
return bytecode.getBytes(UTF_16BE);
75-
}
76-
77-
private static final String GLUE_RESOURCE_PREFIX = "/datadog/instrument/glue/";
78-
79-
private static final int BUFFER_SIZE = 8192;
80-
81-
/**
82-
* Loads glue bytecode with the given name from the given host.
83-
*
84-
* @param host the host class used to load the glue resource
85-
* @param glueName the glue resource containing the bytecode
86-
* @return the glue bytecode
87-
* @throws MissingResourceException if the bytecode cannot be read
88-
*/
89-
@SuppressWarnings({"Since15"})
90-
public static byte[] loadBytecode(Class<?> host, String glueName) {
91-
String glueResource = GLUE_RESOURCE_PREFIX + glueName;
92-
try (InputStream is = Objects.requireNonNull(host.getResourceAsStream(glueResource))) {
93-
if (JVM.atLeastJava(9)) {
94-
return is.readAllBytes();
95-
} else {
96-
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
97-
int bytesRead;
98-
byte[] buf = new byte[BUFFER_SIZE];
99-
while ((bytesRead = is.read(buf, 0, BUFFER_SIZE)) != -1) {
100-
os.write(buf, 0, bytesRead);
101-
}
102-
return os.toByteArray();
103-
}
104-
}
105-
} catch (Throwable e) {
106-
String detail = "Cannot load " + glueResource + " from " + host + ": " + e;
107-
throw new MissingResourceException(detail, host.getName(), glueResource);
108-
}
109-
}
11077
}

utils/src/test/java/datadog/instrument/utils/GlueTest.java renamed to utils/src/test/java/datadog/instrument/glue/GlueGeneratorTest.java

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package datadog.instrument.utils;
1+
package datadog.instrument.glue;
22

33
import static java.util.Collections.singletonList;
44
import static javax.tools.ToolProvider.getSystemJavaCompiler;
@@ -10,38 +10,37 @@
1010
import java.net.URI;
1111
import java.net.URL;
1212
import java.net.URLClassLoader;
13+
import java.nio.file.Files;
1314
import java.util.ArrayList;
1415
import java.util.List;
15-
import java.util.MissingResourceException;
1616
import javax.tools.JavaCompiler;
1717
import javax.tools.SimpleJavaFileObject;
1818
import javax.tools.StandardJavaFileManager;
1919
import javax.tools.StandardLocation;
2020
import org.junit.jupiter.api.Test;
2121
import org.junit.jupiter.api.io.TempDir;
2222

23-
class GlueTest {
24-
25-
private static final byte[] exampleBytecode = {
26-
-54, -2, -70, -66, 0, 0, 0, 52, 0, 10, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106,
27-
97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105,
28-
116, 62, 1, 0, 3, 40, 41, 86, 7, 0, 8, 1, 0, 7, 69, 120, 97, 109, 112, 108, 101, 1, 0, 4, 67,
29-
111, 100, 101, 0, 32, 0, 7, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 6, 0, 1, 0, 9, 0, 0, 0, 17,
30-
0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 0, 0, 0
31-
};
23+
class GlueGeneratorTest {
3224

3325
@Test
34-
void glueGeneration(@TempDir File classesDir) throws Exception {
26+
void glueGenerator(@TempDir File resourceDir, @TempDir File javaDir, @TempDir File classesDir)
27+
throws Exception {
28+
29+
// TestGlueGenerator generates both resource and Java examples of glue
30+
GlueGenerator.main(new String[] {resourceDir.getPath(), javaDir.getPath(), "TestGlue"});
31+
32+
File testGlueResourceFile = new File(resourceDir, "test.glue");
33+
File testGlueJavaFile = new File(javaDir, "TestGlue.java");
3534

35+
assertTrue(testGlueResourceFile.isFile());
36+
assertTrue(testGlueJavaFile.isFile());
37+
38+
// check we can compile the generated Java source
3639
JavaCompiler compiler = getSystemJavaCompiler();
3740
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
3841
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, singletonList(classesDir));
3942

40-
List<String> glueSource = new ArrayList<>();
41-
Glue.classHeader(glueSource, "TestGlue");
42-
glueSource.add(" String BYTECODE =");
43-
Glue.packBytecode(glueSource, exampleBytecode);
44-
glueSource.add("}");
43+
List<String> glueSource = Files.readAllLines(testGlueJavaFile.toPath());
4544

4645
List<InMemoryJavaFile> compilationUnits =
4746
singletonList(new InMemoryJavaFile("datadog/instrument/glue/TestGlue.java", glueSource));
@@ -57,7 +56,16 @@ void glueGeneration(@TempDir File classesDir) throws Exception {
5756
packedBytecode = (String) testGlueClass.getField("BYTECODE").get(null);
5857
}
5958

60-
assertArrayEquals(exampleBytecode, Glue.unpackBytecode(packedBytecode));
59+
byte[] expectedBytecode = {
60+
-54, -2, -70, -66, 0, 0, 0, 52, 0, 10, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106,
61+
97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105,
62+
116, 62, 1, 0, 3, 40, 41, 86, 7, 0, 8, 1, 0, 7, 69, 120, 97, 109, 112, 108, 101, 1, 0, 4, 67,
63+
111, 100, 101, 0, 32, 0, 7, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 6, 0, 1, 0, 9, 0, 0, 0, 17,
64+
0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 0, 0, 0
65+
};
66+
67+
assertArrayEquals(expectedBytecode, Files.readAllBytes(testGlueResourceFile.toPath()));
68+
assertArrayEquals(expectedBytecode, Glue.unpackBytecode(packedBytecode));
6169
}
6270

6371
@Test
@@ -73,20 +81,17 @@ void paddingRequired() {
7381

7482
assertThrows(
7583
IllegalStateException.class,
76-
() -> Glue.packBytecode(new ArrayList<>(), exampleBytecodeNeedsPadding),
84+
() -> GlueGenerator.packBytecode(new ArrayList<>(), exampleBytecodeNeedsPadding),
7785
"Bytecode length is not even; requires padding");
7886
}
7987

8088
@Test
81-
void glueResource() {
82-
byte[] loadedBytecode = Glue.loadBytecode(GlueTest.class, "test.glue");
83-
assertArrayEquals(exampleBytecode, loadedBytecode);
84-
}
89+
void notEnoughArgs() {
8590

86-
@Test
87-
void missingGlueResource() {
8891
assertThrows(
89-
MissingResourceException.class, () -> Glue.loadBytecode(GlueTest.class, "missing.glue"));
92+
IllegalArgumentException.class,
93+
() -> GlueGenerator.main(new String[0]),
94+
"Expected: resource-path java-path glue-name...");
9095
}
9196

9297
static class InMemoryJavaFile extends SimpleJavaFileObject {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package datadog.instrument.glue;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import java.util.MissingResourceException;
7+
import org.junit.jupiter.api.Test;
8+
9+
class GlueTest {
10+
11+
@Test
12+
void glueResource() {
13+
14+
final byte[] expectedBytecode = {
15+
-54, -2, -70, -66, 0, 0, 0, 52, 0, 10, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106,
16+
97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105,
17+
116, 62, 1, 0, 3, 40, 41, 86, 7, 0, 8, 1, 0, 7, 69, 120, 97, 109, 112, 108, 101, 1, 0, 4, 67,
18+
111, 100, 101, 0, 32, 0, 7, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 5, 0, 6, 0, 1, 0, 9, 0, 0, 0, 17,
19+
0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 0, 0, 0
20+
};
21+
22+
assertArrayEquals(expectedBytecode, Glue.loadBytecode(GlueTest.class, "test.glue"));
23+
}
24+
25+
@Test
26+
void missingGlueResource() {
27+
assertThrows(
28+
MissingResourceException.class, () -> Glue.loadBytecode(GlueTest.class, "missing.glue"));
29+
}
30+
}

0 commit comments

Comments
 (0)