├── .classpath ├── .gitignore ├── .project ├── .settings └── org.eclipse.jdt.core.prefs ├── .travis.yml ├── LICENSE ├── MANIFEST.MF ├── README.md ├── build.pro ├── pro_wrapper.java ├── pro_wrapper_settings.txt └── src ├── main └── java │ ├── fr.umlv.mjolnir.agent │ ├── fr │ │ └── umlv │ │ │ └── mjolnir │ │ │ └── agent │ │ │ ├── Agent.java │ │ │ ├── AgentFacadeImpl.java │ │ │ └── Main.java │ └── module-info.java │ ├── fr.umlv.mjolnir.amber │ ├── fr │ │ └── umlv │ │ │ └── mjolnir │ │ │ └── amber │ │ │ ├── Deconstruct.java │ │ │ ├── Pattern.java │ │ │ ├── PatternMatchingMetaFactory.java │ │ │ ├── TupleGenerator.java │ │ │ └── TupleHandle.java │ └── module-info.java │ └── fr.umlv.mjolnir │ ├── fr │ └── umlv │ │ └── mjolnir │ │ ├── AgentFacade.java │ │ ├── Mjolnir.java │ │ ├── OverrideEntryPoint.java │ │ ├── bytecode │ │ ├── AnnotationOracle.java │ │ └── Rewriter.java │ │ ├── log │ │ ├── Log.java │ │ └── OldLog.java │ │ └── util │ │ └── stream │ │ ├── LoopBuilder.java │ │ └── Stream.java │ └── module-info.java └── test └── java ├── fr.umlv.mjolnir.amber ├── fr │ └── umlv │ │ └── mjolnir │ │ └── amber │ │ ├── PatternMatchingTests.java │ │ └── TupleHandleTests.java └── module-info.java └── fr.umlv.mjolnir ├── fr └── umlv │ └── mjolnir │ ├── LogTests.java │ └── MjolnirTests.java └── module-info.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | /target/ 24 | /pro/ 25 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | mjolnir 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=13 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=13 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 11 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 12 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning 13 | org.eclipse.jdt.core.compiler.release=enabled 14 | org.eclipse.jdt.core.compiler.source=13 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: openjdk11 3 | 4 | install: 5 | - java pro_wrapper.java version 6 | 7 | script: 8 | - ./pro/bin/pro 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rémi Forax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Can-Retransform-Classes: true 2 | Premain-Class: fr.umlv.mjolnir.agent.Agent 3 | Launcher-Agent-Class: fr.umlv.mjolnir.agent.Agent 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mjolnir 2 | Thor Hammer and secondarily a way to express invokedynamic in Java 3 | 4 | [![Mjolnir build status](https://api.travis-ci.org/forax/mjolnir.svg?branch=master)](https://travis-ci.org/forax/mjolnir) 5 | 6 | ## Goal 7 | Mjolnir is a Java class allowing to initialize a stable value by calling a bootstrap method once. 8 | 9 | The implementation is optimized so the stable value is very cheap to get. 10 | A bytecode rewriter is provided to replace the access to the stable value by an invokedynamic making the call even cheaper (mostly free). 11 | 12 | Mjolnir has the following properties: 13 | - Mjolnir.get().invokeExact() is semantically equivalent to an invokedynamic 14 | - initialize the constant with a bootstrap method 15 | no bootstrap call in the fast path 16 | - no boxing of arguments 17 | - no static analysis requires for the bytecode rewriter 18 | - crawling the bytecode is enough 19 | - should work without the bytecode rewriter (for testing) 20 | 21 | ## Video 22 | 23 | [![Me presenting Mjolnir at JVM Summit 2017](https://img.youtube.com/vi/Rco7hcOM7Ig/0.jpg)](https://www.youtube.com/embed/Rco7hcOM7Ig?list=PLX8CzqL3ArzXJ2EGftrmz4SzS6NRr6p2n "Me presenting Mjolnir at JVM Summit 2017") 24 | 25 | ## How to build it 26 | 27 | Mjolnir is built using [pro](https://github.com/forax/pro) which is my own build tool, you can download it from github (amazon S3) like this 28 | ``` 29 | sh get_pro.sh 30 | ``` 31 | (if you are not on linux, you have to build pro by yourself, sorry) 32 | 33 | and build Mjolnir just by running pro. 34 | ``` 35 | pro/bin/pro 36 | ``` 37 | 38 | ## Examples 39 | 40 | The following example implements the equivalent of the macro__LINE__ i.e. it returns the current line number like in C 41 | ```java 42 | private static int boostrap(Lookup lookup) { 43 | String className = lookup.lookupClass().getName(); 44 | int lineNumber = StackWalker.getInstance() 45 | .walk(s -> s.skip(1).filter(f -> f.getClassName().equals(className)).findFirst()) 46 | .get() 47 | .getLineNumber(); 48 | return lineNumber; 49 | } 50 | 51 | public static void main(String[] args) { 52 | int __LINE__ = Mjolnir.get(lookup -> boostrap(lookup)); 53 | } 54 | ``` 55 | 56 | This mechanism can be used to express an invokedynamic in Java, the bootstrap method can return a MethodHandle 57 | that will be called with invokeExact. 58 | 59 | ```java 60 | private static String hello(String name) { 61 | return "Hello " + name; 62 | } 63 | 64 | private static MethodHandle initHello(Lookup lookup) throws NoSuchMethodException, IllegalAccessException { 65 | Class declaringClass = lookup.lookupClass(); 66 | return lookup.findStatic(declaringClass, "hello", methodType(String.class, String.class)); 67 | } 68 | 69 | public static void main(String[] args) hello() throws Throwable { 70 | String result = (String)Mjolnir.get(lookup -> initHello(lookup)).invokeExact("Mjolnir"); 71 | System.out.println(result); // Hello Mjolnir 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /build.pro: -------------------------------------------------------------------------------- 1 | import static com.github.forax.pro.Pro.* 2 | import static com.github.forax.pro.builder.Builders.* 3 | 4 | pro.loglevel("verbose") 5 | pro.exitOnError(true) 6 | 7 | resolver. 8 | checkForUpdate(true). 9 | dependencies( 10 | // ASM 9 11 | "org.objectweb.asm=org.ow2.asm:asm:9.0", 12 | "org.objectweb.asm.util=org.ow2.asm:asm-util:9.0", 13 | "org.objectweb.asm.tree=org.ow2.asm:asm-tree:9.0", 14 | "org.objectweb.asm.tree.analysis=org.ow2.asm:asm-analysis:9.0", 15 | 16 | // JUnit 5 17 | "org.junit.jupiter.api=org.junit.jupiter:junit-jupiter-api:5.7.0", 18 | "org.junit.platform.commons=org.junit.platform:junit-platform-commons:1.7.0", 19 | "org.apiguardian.api=org.apiguardian:apiguardian-api:1.1.0", 20 | "org.opentest4j=org.opentest4j:opentest4j:1.2.0" 21 | ) 22 | 23 | compiler.lint("all,-varargs,-overloads") 24 | 25 | packager.moduleMetadata( 26 | "fr.umlv.mjolnir@1.0/fr.umlv.mjolnir.bytecode.Rewriter", 27 | "fr.umlv.mjolnir.agent@1.0/fr.umlv.mjolnir.agent.Main", 28 | "fr.umlv.mjolnir.amber@1.0/fr.umlv.mjolnir.amber.Main" 29 | ) 30 | packager.rawArguments( 31 | "--manifest=MANIFEST.MF" 32 | ) 33 | 34 | // the runner will rewrite the bytecode when called 35 | runner.module("fr.umlv.mjolnir") 36 | 37 | tester.timeout(99) 38 | 39 | // run the test once without bytecode modification and again after bytecode rewriting 40 | run(resolver, modulefixer, compiler, packager, tester, runner, tester) 41 | 42 | 43 | // test agent 44 | runner.module("fr.umlv.mjolnir.agent") 45 | runner.rawArguments( 46 | "-javaagent:target/main/artifact/fr.umlv.mjolnir.agent-1.0.jar" 47 | ) 48 | run(runner) 49 | 50 | /exit -------------------------------------------------------------------------------- /pro_wrapper.java: -------------------------------------------------------------------------------- 1 | import static java.lang.System.exit; 2 | import static java.lang.System.getProperty; 3 | import static java.nio.file.Files.createDirectories; 4 | import static java.nio.file.Files.delete; 5 | import static java.nio.file.Files.exists; 6 | import static java.nio.file.Files.getLastModifiedTime; 7 | import static java.nio.file.Files.getPosixFilePermissions; 8 | import static java.nio.file.Files.list; 9 | import static java.nio.file.Files.newBufferedReader; 10 | import static java.nio.file.Files.newOutputStream; 11 | import static java.nio.file.Files.readAllLines; 12 | import static java.nio.file.Files.setPosixFilePermissions; 13 | import static java.nio.file.Files.walk; 14 | import static java.nio.file.Files.walkFileTree; 15 | import static java.nio.file.Files.write; 16 | import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; 17 | import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; 18 | import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; 19 | import static java.util.Collections.reverseOrder; 20 | import static java.util.Comparator.comparing; 21 | import static java.util.function.Predicate.not; 22 | 23 | import java.io.BufferedReader; 24 | import java.io.IOException; 25 | import java.io.InputStreamReader; 26 | import java.io.UncheckedIOException; 27 | import java.net.InetAddress; 28 | import java.net.URL; 29 | import java.net.UnknownHostException; 30 | import java.nio.charset.StandardCharsets; 31 | import java.nio.file.FileVisitResult; 32 | import java.nio.file.Files; 33 | import java.nio.file.Path; 34 | import java.nio.file.SimpleFileVisitor; 35 | import java.nio.file.attribute.BasicFileAttributes; 36 | import java.nio.file.attribute.FileTime; 37 | import java.util.Arrays; 38 | import java.util.Collections; 39 | import java.util.List; 40 | import java.util.Optional; 41 | import java.util.Properties; 42 | import java.util.regex.Matcher; 43 | import java.util.regex.Pattern; 44 | import java.util.stream.Stream; 45 | import java.util.zip.ZipFile; 46 | 47 | class pro_wrapper { 48 | private static final String GITHUB_API_RELEASES = "https://github.com/forax/pro/releases"; 49 | private static final String GITHUB_DOWNLOAD = "https://github.com/forax/pro/releases/download"; 50 | private static final Pattern PATTERN = Pattern.compile("tag/([^\"]+)\""); 51 | 52 | private static String platform() { 53 | var osName = getProperty("os.name").toLowerCase(); 54 | if (osName.indexOf("win") != -1) { 55 | return "windows"; 56 | } 57 | if (osName.indexOf("mac") != -1) { 58 | return "macos"; 59 | } 60 | return "linux"; 61 | } 62 | 63 | private static String shell() { 64 | var osName = getProperty("os.name").toLowerCase(); 65 | if (osName.indexOf("win") != -1) { 66 | return "cmd.exe"; 67 | } 68 | return Optional.ofNullable(System.getenv("SHELL")).filter(not(String::isEmpty)).orElse("/bin/sh"); 69 | } 70 | 71 | private static Optional specialBuild() throws IOException { 72 | var specialBuild = System.getenv("PRO_SPECIAL_BUILD"); 73 | var path = Path.of("pro_wrapper_settings.txt"); 74 | var proSettings = new Properties(); 75 | if (exists(path)) { 76 | try(var reader = newBufferedReader(path)) { 77 | proSettings.load(reader); 78 | } 79 | } 80 | return Optional.ofNullable(specialBuild).filter(not(String::isEmpty)).or( 81 | () -> Optional.ofNullable(proSettings.getProperty("PRO_SPECIAL_BUILD"))); 82 | } 83 | 84 | private static String userHome() { 85 | return System.getProperty("user.home"); 86 | } 87 | 88 | private static Optional lastestReleaseVersionFromGitHub() throws IOException { 89 | System.out.println("try to find latest release on Github ..."); 90 | var url = new URL(GITHUB_API_RELEASES); 91 | try { 92 | InetAddress.getByName(url.getHost()); // is internet accessible ? 93 | } catch(@SuppressWarnings("unused") UnknownHostException e) { 94 | return Optional.empty(); 95 | } 96 | try(var input = url.openStream(); 97 | var reader = new InputStreamReader(input, StandardCharsets.UTF_8); 98 | var buffered = new BufferedReader(reader, 8192)) { 99 | return buffered.lines() 100 | .flatMap(line -> Optional.of(PATTERN.matcher(line)).filter(Matcher::find).map(matcher -> matcher.group(1)).stream()) 101 | .findFirst(); 102 | } 103 | } 104 | 105 | private static FileTime lastModified(Path path) { 106 | try { 107 | return getLastModifiedTime(path); 108 | } catch (IOException e) { 109 | throw new UncheckedIOException(e); 110 | } 111 | } 112 | 113 | private static Optional latestReleaseVersionFromCache(Path cache, String filename) { 114 | System.out.println("try to find latest release in the cache ..."); 115 | try { 116 | return walk(cache) 117 | .filter(p -> p.getFileName().toString().equals(filename)) 118 | .sorted(reverseOrder(comparing(p -> lastModified(p)))) 119 | .findFirst().map(p -> p.getParent().getFileName().toString()); 120 | } catch (@SuppressWarnings("unused") IOException | UncheckedIOException e) { 121 | return Optional.empty(); 122 | } 123 | } 124 | 125 | private static void download(String release, String filename, Path path) throws IOException { 126 | var url = new URL(GITHUB_DOWNLOAD + "/"+ release + "/" + filename); 127 | System.out.println("download " + url + " to " + path); 128 | 129 | createDirectories(path.getParent()); 130 | try(var input = url.openStream(); 131 | var output = newOutputStream(path)) { 132 | int read; 133 | var sum = 0; 134 | var buffer = new byte[8192]; 135 | while((read = input.read(buffer)) != -1) { 136 | output.write(buffer, 0, read); 137 | 138 | sum += read; 139 | if (sum >= 1_000_000) { 140 | System.out.print("."); 141 | sum = 0; 142 | } 143 | } 144 | } 145 | System.out.println(""); 146 | } 147 | 148 | private static void setExecutionPermissions(Path path) throws IOException { 149 | var permissions = getPosixFilePermissions(path); 150 | Collections.addAll(permissions, OWNER_EXECUTE, GROUP_EXECUTE, OTHERS_EXECUTE); 151 | setPosixFilePermissions(path, permissions); 152 | } 153 | 154 | private static void unpack(Path localPath, Path folder) throws IOException { 155 | System.out.println("unpack pro to " + folder); 156 | createDirectories(folder); 157 | try(var zip = new ZipFile(localPath.toFile())) { 158 | for(var entry: Collections.list(zip.entries())) { 159 | var path = folder.resolve(entry.getName()); 160 | if (entry.isDirectory()) { 161 | createDirectories(path); 162 | continue; 163 | } 164 | try(var input = zip.getInputStream(entry); 165 | var output = newOutputStream(path)) { 166 | input.transferTo(output); 167 | } 168 | } 169 | } 170 | 171 | // make the commands in pro/bin, and pro/lib executable 172 | for(var cmd: (Iterable)list(folder.resolve("pro").resolve("bin"))::iterator) { 173 | setExecutionPermissions(cmd); 174 | } 175 | for(var path: (Iterable)list(folder.resolve("pro").resolve("lib"))::iterator) { 176 | var fileName = path.getFileName().toString(); 177 | if (fileName.equals("jexec") || fileName.equals("jspawnhelper")) { 178 | setExecutionPermissions(path); 179 | } 180 | } 181 | } 182 | 183 | private static void deleteAllFiles(Path directory) throws IOException { 184 | if (!exists(directory)) { 185 | return; 186 | } 187 | 188 | walkFileTree(directory, new SimpleFileVisitor<>() { 189 | @Override 190 | public FileVisitResult postVisitDirectory(Path path, IOException e) throws IOException { 191 | delete(path); 192 | return super.postVisitDirectory(path, e); 193 | } 194 | @Override 195 | public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { 196 | delete(path); 197 | return FileVisitResult.CONTINUE; 198 | } 199 | }); 200 | } 201 | 202 | private static String firstLine(Path path) throws IOException { 203 | return Optional.of(readAllLines(path)).filter(lines -> !lines.isEmpty()).map(lines -> lines.get(0)).orElse(""); 204 | } 205 | 206 | private static int exec(String command, String[] args) throws IOException { 207 | var builder = new ProcessBuilder(Stream.of(Stream.of(shell()), Stream.of(command), Arrays.stream(args)).flatMap(x -> x).toArray(String[]::new)); 208 | var process = builder.inheritIO().start(); 209 | try { 210 | return process.waitFor(); 211 | } catch (InterruptedException e) { 212 | throw new IOException(e); 213 | } 214 | } 215 | 216 | public interface PathConsumer { 217 | void accept(Path path) throws IOException; 218 | } 219 | 220 | private static void retry(Path resource, int times, PathConsumer consumer) throws IOException { 221 | if (times <= 0) { 222 | throw new IllegalArgumentException("times <= 0"); 223 | } 224 | IOException exception = null; 225 | var count = times; 226 | do { 227 | try { 228 | consumer.accept(resource); 229 | return; 230 | } catch(IOException e) { 231 | if (exception == null) { 232 | exception = new IOException(); 233 | } 234 | exception.addSuppressed(e); 235 | 236 | // cleanup 237 | Files.deleteIfExists(resource); 238 | 239 | System.out.println("download fails ... retry !"); 240 | } 241 | } while(--count != 0); 242 | throw exception; 243 | } 244 | 245 | private static int installAndRun(String[] args) throws IOException { 246 | var specialBuild = specialBuild().map(build -> '-' + build).orElse(""); 247 | var filename = "pro-" + platform() + specialBuild + ".zip"; 248 | 249 | var cache = Path.of(userHome(), ".pro", "cache"); 250 | var release = lastestReleaseVersionFromGitHub() 251 | .or(() -> latestReleaseVersionFromCache(cache, filename)) 252 | .orElseThrow(() -> new IOException("no release found !")); 253 | 254 | 255 | System.out.println("require " + filename + " release " + release + " ..."); 256 | 257 | var cachePath = cache.resolve(Path.of(release, filename)); 258 | if (!exists(cachePath)) { 259 | retry(cachePath, 3, _cachedPath -> download(release, filename, _cachedPath)); 260 | } 261 | 262 | var releaseTxt = Path.of("pro", "pro-release.txt"); 263 | if (!exists(releaseTxt) || !firstLine(releaseTxt).equals(filename)) { 264 | deleteAllFiles(releaseTxt.getParent()); 265 | 266 | unpack(cachePath, Path.of(".")); 267 | write(releaseTxt, List.of(filename)); 268 | } 269 | 270 | return exec("pro/bin/pro", args); 271 | } 272 | 273 | public static void main(String[] args) { 274 | try { 275 | exit(installAndRun(args)); 276 | } catch(IOException e) { 277 | System.err.println("i/o error " + e.getMessage() + 278 | Optional.ofNullable(e.getStackTrace()).filter(stack -> stack.length > 0).map(stack -> " at " + stack[0]).orElse("")); 279 | exit(1); 280 | } 281 | } 282 | } -------------------------------------------------------------------------------- /pro_wrapper_settings.txt: -------------------------------------------------------------------------------- 1 | PRO_SPECIAL_BUILD=early-access 2 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.agent/fr/umlv/mjolnir/agent/Agent.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.agent; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.lang.instrument.ClassFileTransformer; 7 | import java.lang.instrument.IllegalClassFormatException; 8 | import java.lang.instrument.Instrumentation; 9 | import java.security.ProtectionDomain; 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | 13 | import fr.umlv.mjolnir.bytecode.Rewriter; 14 | 15 | //import fr.umlv.mjolnir.bytecode.Rewriter; 16 | 17 | class Agent { 18 | static Instrumentation instrumentation; 19 | 20 | public static void premain(String agentArgs, Instrumentation instrumentation) { 21 | 22 | //public static void agentmain(String agentArgs, Instrumentation instrumentation) { 23 | //System.out.println("agent started"); 24 | 25 | 26 | Agent.instrumentation = instrumentation; 27 | 28 | instrumentation.addTransformer(new ClassFileTransformer() { 29 | @Override 30 | public byte[] transform(Module module, ClassLoader loader, String className, Class classBeingRedefined, 31 | ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 32 | 33 | //System.out.println("transform " + className + " " + classBeingRedefined); 34 | 35 | if (classBeingRedefined == null) { // do not rewrite too early 36 | return null; 37 | } 38 | 39 | Function> classFileFinder = internalName -> Optional.ofNullable(loader.getResourceAsStream(internalName + ".class")); 40 | try { 41 | return Rewriter.rewrite(new ByteArrayInputStream(classfileBuffer), classFileFinder); 42 | } catch (IOException e) { 43 | throw (IllegalClassFormatException)new IllegalClassFormatException().initCause(e); 44 | } 45 | } 46 | }, true); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.agent/fr/umlv/mjolnir/agent/AgentFacadeImpl.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.agent; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.lang.instrument.ClassDefinition; 6 | import java.lang.instrument.Instrumentation; 7 | import java.lang.instrument.UnmodifiableClassException; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.function.Function; 12 | 13 | import fr.umlv.mjolnir.AgentFacade; 14 | import fr.umlv.mjolnir.bytecode.Rewriter; 15 | 16 | public class AgentFacadeImpl implements AgentFacade { 17 | private Instrumentation checkInstrumentation() { 18 | Instrumentation instrumentation = Agent.instrumentation; 19 | if (instrumentation == null) { 20 | throw new IllegalStateException("no instrumentation"); 21 | } 22 | return instrumentation; 23 | } 24 | 25 | @Override 26 | public void rewriteIfPossible(Class declaringClass) throws IllegalStateException { 27 | Instrumentation instrumentation = checkInstrumentation(); 28 | 29 | /* 30 | byte[] bytecode; 31 | ClassLoader loader = declaringClass.getClassLoader(); 32 | try(InputStream input = loader.getResourceAsStream(declaringClass.getName().replace('.', '/') + ".class")) { 33 | if (input == null) { 34 | throw new IllegalStateException("no input"); 35 | } 36 | 37 | Function> classFileFinder = internalName -> Optional.ofNullable(loader.getResourceAsStream(internalName + ".class")); 38 | bytecode = Rewriter.rewrite(input, classFileFinder); 39 | } catch (IOException e) { 40 | throw new IllegalStateException(e); 41 | } 42 | 43 | try { 44 | instrumentation.redefineClasses(new ClassDefinition(declaringClass, bytecode)); 45 | } catch (ClassNotFoundException | UnmodifiableClassException e) { 46 | throw new IllegalStateException(e); 47 | }*/ 48 | 49 | try { 50 | instrumentation.retransformClasses(declaringClass); 51 | } catch (UnmodifiableClassException e) { 52 | throw new IllegalStateException(e); 53 | } 54 | } 55 | 56 | @Override 57 | public void addReads(Module source, Module destination) throws IllegalStateException { 58 | Instrumentation instrumentation = checkInstrumentation(); 59 | instrumentation.redefineModule(source, Set.of(destination), Map.of(), Map.of(), Set.of(), Map.of()); 60 | } 61 | 62 | @Override 63 | public void addOpens(Module source, String packaze, Module destination) { 64 | Instrumentation instrumentation = checkInstrumentation(); 65 | instrumentation.redefineModule(source, Set.of(), Map.of(), Map.of(packaze, Set.of(destination)), Set.of(), Map.of()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.agent/fr/umlv/mjolnir/agent/Main.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.agent; 2 | 3 | import java.util.function.IntUnaryOperator; 4 | 5 | import fr.umlv.mjolnir.Mjolnir; 6 | import fr.umlv.mjolnir.Mjolnir.Bootstrap; 7 | 8 | public class Main { 9 | private static int incr(int i) { 10 | return Mjolnir.get((Bootstrap)__ -> v -> v + 1).applyAsInt(i); 11 | } 12 | 13 | private static void loop() { 14 | int i = 0; 15 | while (i < 10_000_000) { 16 | i = incr(i); 17 | } 18 | } 19 | 20 | public static void main(String[] args) { 21 | String message = Mjolnir.get(lookup -> "Hello Mjolnir"); 22 | System.out.println(message); 23 | 24 | loop(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.agent/module-info.java: -------------------------------------------------------------------------------- 1 | open module fr.umlv.mjolnir.agent { 2 | //requires jdk.attach; 3 | requires java.instrument; 4 | requires transitive fr.umlv.mjolnir; 5 | 6 | exports fr.umlv.mjolnir.agent; 7 | 8 | provides fr.umlv.mjolnir.AgentFacade with fr.umlv.mjolnir.agent.AgentFacadeImpl; 9 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/Deconstruct.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Deconstruct { 11 | Class[] value(); 12 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/Pattern.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | import static java.lang.invoke.MethodHandles.dropArguments; 5 | import static java.lang.invoke.MethodHandles.guardWithTest; 6 | import static java.lang.invoke.MethodHandles.identity; 7 | import static java.lang.invoke.MethodHandles.insertArguments; 8 | import static java.lang.invoke.MethodType.methodType; 9 | 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.invoke.MethodHandles; 12 | import java.lang.invoke.MethodHandles.Lookup; 13 | import java.lang.invoke.MethodType; 14 | import java.lang.reflect.Method; 15 | import java.util.Arrays; 16 | import java.util.function.Function; 17 | 18 | import fr.umlv.mjolnir.amber.TupleHandle.Form; 19 | 20 | public class Pattern { 21 | private final Form form; 22 | private final Function mh; 23 | 24 | Pattern(Form form, Function mh) { 25 | this.form = form; 26 | this.mh = mh; 27 | } 28 | 29 | public Form form() { 30 | return form; 31 | } 32 | 33 | public MethodHandle create() { 34 | //System.out.println("create mh: form " + form); 35 | return mh.apply(form); 36 | } 37 | 38 | private static MethodHandle noMatchMH(Form uberForm) { 39 | Object gotoDefault; 40 | try { 41 | gotoDefault = uberForm.createAs(methodType(void.class, int.class, boolean.class)).constructor().invokeExact(-1, false); 42 | } catch (Throwable e) { 43 | throw new AssertionError(e); 44 | } 45 | return dropArguments(constant(Object.class, gotoDefault), 0, Object.class); 46 | } 47 | 48 | public static Pattern noMatch() { 49 | Form form = Form.of(methodType(void.class, int.class, boolean.class)); 50 | return new Pattern(form, Pattern::noMatchMH); 51 | } 52 | 53 | public Pattern or(Pattern match) { 54 | return new Pattern(form.or(match.form), 55 | uberForm -> MethodHandles.permuteArguments( 56 | MethodHandles.filterArguments( 57 | guardWithTest( 58 | dropArguments( 59 | uberForm.createAs(methodType(void.class, int.class, boolean.class)).component(1), 60 | 0, Object.class), 61 | dropArguments(identity(Object.class), 0, Object.class), 62 | dropArguments(match.mh.apply(uberForm), 1, Object.class)), 63 | 1, mh.apply(uberForm)), 64 | methodType(Object.class, Object.class), new int[] { 0, 0})); 65 | } 66 | 67 | public static Pattern match(Lookup lookup, Class type, int selector) { 68 | Method deconstructor = Arrays.stream(type.getMethods()) 69 | .filter(m -> m.isAnnotationPresent(Deconstruct.class)) 70 | .findFirst() 71 | .get(); // feel lucky ? 72 | 73 | Deconstruct deconstruct = deconstructor.getAnnotation(Deconstruct.class); 74 | MethodHandle target; 75 | try { 76 | target = lookup.unreflect(deconstructor) 77 | .asType(methodType(Object.class, Object.class, MethodHandle.class)); 78 | } catch (IllegalAccessException e) { 79 | throw (LinkageError)new LinkageError().initCause(e); 80 | } 81 | MethodType methodType = PatternMatchingMetaFactory.type(deconstruct.value()); 82 | 83 | //System.out.println("match " + type + " " + methodType); 84 | //System.out.println(" deconstructor " + target); 85 | 86 | return new Pattern(Form.of(methodType), 87 | uberForm -> guardWithTest( 88 | IS_INSTANCE.bindTo(type), 89 | insertArguments(target, 1, insertArguments(uberForm.createAs(methodType).constructor(), 0, selector)), 90 | noMatchMH(uberForm))); 91 | } 92 | 93 | private static MethodHandle IS_INSTANCE; 94 | static { 95 | try { 96 | IS_INSTANCE = MethodHandles.lookup().findVirtual(Class.class, "isInstance", methodType(boolean.class, Object.class)); 97 | } catch (NoSuchMethodException | IllegalAccessException e) { 98 | throw new AssertionError(e); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/PatternMatchingMetaFactory.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles.Lookup; 5 | import java.lang.invoke.MethodType; 6 | import java.util.Arrays; 7 | import java.util.stream.Stream; 8 | 9 | import static java.lang.invoke.MethodType.methodType; 10 | 11 | public class PatternMatchingMetaFactory { 12 | public static MethodHandle indy(Lookup lookup, Pattern pattern) { 13 | return pattern.create(); 14 | } 15 | 16 | public static MethodHandle component(Lookup lookup, MethodType type, int index, Pattern pattern) { 17 | TupleHandle handle = pattern.form().createAs(type); 18 | return handle.component(index); 19 | } 20 | 21 | public static Pattern condy(Lookup lookup, Class... classes) { 22 | Pattern pattern = Pattern.noMatch(); 23 | for(int i = 0; i < classes.length; i++) { 24 | Pattern match = Pattern.match(lookup, classes[i], i); 25 | pattern = pattern.or(match); 26 | } 27 | return pattern; 28 | } 29 | 30 | public static MethodType type(Class... classes) { 31 | return methodType(void.class, Stream.concat(Stream.of(int.class, boolean.class), Arrays.stream(classes)).toArray(Class[]::new)); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/TupleGenerator.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import static java.lang.invoke.MethodType.methodType; 4 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 5 | import static org.objectweb.asm.Opcodes.ACC_FINAL; 6 | import static org.objectweb.asm.Opcodes.ACC_PRIVATE; 7 | import static org.objectweb.asm.Opcodes.ACC_PUBLIC; 8 | import static org.objectweb.asm.Opcodes.ACC_STATIC; 9 | import static org.objectweb.asm.Opcodes.ACC_SUPER; 10 | import static org.objectweb.asm.Opcodes.ALOAD; 11 | import static org.objectweb.asm.Opcodes.ARETURN; 12 | import static org.objectweb.asm.Opcodes.DUP; 13 | import static org.objectweb.asm.Opcodes.INVOKESPECIAL; 14 | import static org.objectweb.asm.Opcodes.LLOAD; 15 | import static org.objectweb.asm.Opcodes.NEW; 16 | import static org.objectweb.asm.Opcodes.PUTFIELD; 17 | import static org.objectweb.asm.Opcodes.RETURN; 18 | import static org.objectweb.asm.Opcodes.V9; 19 | 20 | import java.io.PrintWriter; 21 | import java.lang.invoke.MethodHandle; 22 | import java.lang.invoke.MethodHandles; 23 | import java.lang.invoke.MethodHandles.Lookup; 24 | import java.lang.invoke.MethodType; 25 | import java.util.ArrayList; 26 | 27 | import org.objectweb.asm.ClassReader; 28 | import org.objectweb.asm.ClassWriter; 29 | import org.objectweb.asm.FieldVisitor; 30 | import org.objectweb.asm.MethodVisitor; 31 | import org.objectweb.asm.util.CheckClassAdapter; 32 | 33 | //import sun.misc.Unsafe; 34 | 35 | import fr.umlv.mjolnir.amber.TupleHandle.Form; 36 | 37 | class TupleGenerator { 38 | private static final Lookup LOOKUP = MethodHandles.lookup(); 39 | private static final String CLASS_PREFIX = TupleHandle.class.getPackageName().replace('.', '/') + "/FORM"; 40 | 41 | private static MethodHandle constructor(Class clazz, MethodType methodType) { 42 | MethodHandle constructor; 43 | try { 44 | constructor = LOOKUP.findStatic(clazz, "create", methodType.changeReturnType(clazz)); 45 | } catch (IllegalAccessException | NoSuchMethodException e) { 46 | throw (LinkageError)new LinkageError().initCause(e); 47 | } 48 | return constructor.asType(constructor.type().changeReturnType(Object.class)); 49 | } 50 | 51 | private static MethodHandle[] getters(Class clazz, Form form) { 52 | ArrayList mhs = new ArrayList<>(); 53 | for(int i = 0; i < form.objects; i++) { 54 | mhs.add(getter(clazz, "objects$" + i, Object.class)); 55 | } 56 | for(int i = 0; i < form.prims; i++) { 57 | mhs.add(getter(clazz, "prims$" + i, long.class)); 58 | } 59 | return mhs.toArray(new MethodHandle[0]); 60 | } 61 | 62 | private static MethodHandle getter(Class clazz, String name, Class type) { 63 | MethodHandle getter; 64 | try { 65 | getter = LOOKUP.findGetter(clazz, name, type); 66 | } catch (IllegalAccessException | NoSuchFieldException e) { 67 | throw (LinkageError)new LinkageError().initCause(e); 68 | } 69 | return getter.asType(methodType(type, Object.class)); 70 | } 71 | 72 | 73 | public static TupleHandle generate(Form form) { 74 | MethodType methodType = form.asMethodType(); 75 | Class clazz = generate(form, methodType); 76 | return new TupleHandle(methodType, constructor(clazz, methodType.changeReturnType(clazz)), getters(clazz, form)); 77 | } 78 | 79 | private static Class generate(Form form, MethodType methodType) { 80 | String className = CLASS_PREFIX + form.objects + '_' + form.prims; 81 | ClassWriter writer = new ClassWriter(COMPUTE_MAXS); 82 | writer.visit(V9, ACC_PUBLIC| ACC_SUPER, className, null, "java/lang/Object", null); 83 | 84 | for(int i = 0; i < form.objects; i++) { 85 | FieldVisitor fv = writer.visitField(ACC_PUBLIC|ACC_FINAL, "objects$" + i, "Ljava/lang/Object;", null, null); 86 | fv.visitEnd(); 87 | } 88 | for(int i = 0; i < form.prims; i++) { 89 | FieldVisitor fv = writer.visitField(ACC_PUBLIC|ACC_FINAL, "prims$" + i, "J", null, null); 90 | fv.visitEnd(); 91 | } 92 | 93 | String desc = methodType.toMethodDescriptorString(); 94 | { 95 | MethodVisitor mv = writer.visitMethod(ACC_PRIVATE, "", desc, null, null); 96 | mv.visitCode(); 97 | mv.visitVarInsn(ALOAD, 0); 98 | mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); 99 | for(int i = 0; i < form.objects; i++) { 100 | mv.visitVarInsn(ALOAD, 0); 101 | mv.visitVarInsn(ALOAD, i + 1); 102 | mv.visitFieldInsn(PUTFIELD, className, "objects$" + i, "Ljava/lang/Object;"); 103 | } 104 | for(int i = 0; i < form.prims; i++) { 105 | mv.visitVarInsn(ALOAD, 0); 106 | mv.visitVarInsn(LLOAD, form.objects + 1 + i * 2); 107 | mv.visitFieldInsn(PUTFIELD, className, "prims$" + i, "J"); 108 | } 109 | mv.visitInsn(RETURN); 110 | mv.visitMaxs(-1, -1); 111 | mv.visitEnd(); 112 | } 113 | 114 | String factoryDesc = desc.substring(0, desc.length() - 1) + 'L' + className + ';'; // replace V by className 115 | { 116 | MethodVisitor mv = writer.visitMethod(ACC_PUBLIC|ACC_STATIC, "create", factoryDesc, null, null); 117 | mv.visitCode(); 118 | mv.visitTypeInsn(NEW, className); 119 | mv.visitInsn(DUP); 120 | for(int i = 0; i < form.objects; i++) { 121 | mv.visitVarInsn(ALOAD, i); 122 | } 123 | for(int i = 0; i < form.prims; i++) { 124 | mv.visitVarInsn(LLOAD, form.objects + i * 2); 125 | } 126 | mv.visitMethodInsn(INVOKESPECIAL, className, "", desc, false); 127 | mv.visitInsn(ARETURN); 128 | mv.visitMaxs(-1, -1); 129 | mv.visitEnd(); 130 | } 131 | 132 | writer.visitEnd(); 133 | 134 | byte[] code = writer.toByteArray(); 135 | 136 | // DEBUG 137 | CheckClassAdapter.verify(new ClassReader(code), false, new PrintWriter(System.out)); 138 | 139 | try { 140 | return LOOKUP.defineClass(code); 141 | } catch (IllegalAccessException e) { 142 | throw (LinkageError)new LinkageError().initCause(e); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/TupleHandle.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import static java.lang.invoke.MethodHandles.explicitCastArguments; 4 | import static java.lang.invoke.MethodHandles.filterArguments; 5 | import static java.lang.invoke.MethodHandles.filterReturnValue; 6 | import static java.lang.invoke.MethodHandles.identity; 7 | import static java.lang.invoke.MethodHandles.permuteArguments; 8 | import static java.lang.invoke.MethodType.methodType; 9 | 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.invoke.MethodHandles; 12 | import java.lang.invoke.MethodHandles.Lookup; 13 | import java.lang.invoke.MethodType; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.stream.IntStream; 19 | 20 | public final class TupleHandle { 21 | private static final ConcurrentHashMap FORM_CACHE = new ConcurrentHashMap<>(); 22 | 23 | private final MethodType type; 24 | private final MethodHandle constructor; 25 | private final MethodHandle[] components; 26 | 27 | TupleHandle(MethodType type, MethodHandle constructor, MethodHandle[] components) { 28 | this.type = type; 29 | this.constructor = constructor; 30 | this.components = components; 31 | } 32 | 33 | public MethodType type() { 34 | return type; 35 | } 36 | 37 | public Form form() { 38 | return Form.of(type); 39 | } 40 | 41 | public MethodHandle constructor() { 42 | return constructor; 43 | } 44 | 45 | public MethodHandle component(int index) { 46 | return components[index]; 47 | } 48 | 49 | public static TupleHandle create(Class... parameterTypes) { 50 | return create(MethodType.methodType(void.class, parameterTypes)); 51 | } 52 | 53 | public static TupleHandle create(MethodType type) { 54 | return createImpl(Form.of(type), type); 55 | } 56 | 57 | static TupleHandle createImpl(Form form, MethodType type) { 58 | if (type.returnType() != void.class) { 59 | throw new IllegalArgumentException(); 60 | } 61 | TupleHandle handle = FORM_CACHE.get(form); 62 | if (handle == null) { 63 | handle = TupleGenerator.generate(form); 64 | FORM_CACHE.putIfAbsent(form, handle); 65 | } 66 | return handle.adapt(type, form); 67 | } 68 | 69 | private static MethodType erase(MethodType methodType) { 70 | return methodType(methodType.returnType(), 71 | methodType.parameterList().stream() 72 | .map(type -> type.isPrimitive()? long.class: Object.class) 73 | .toArray(Class[]::new)); 74 | } 75 | 76 | private static Object[] values(Object value, int length) { 77 | return IntStream.range(0, length).mapToObj(__ -> value).toArray(); 78 | } 79 | private static Class[] classes(Class value, int length) { 80 | return IntStream.range(0, length).mapToObj(__ -> value).toArray(Class[]::new); 81 | } 82 | 83 | private TupleHandle adapt(MethodType type, Form form) { 84 | if (this.type.equals(type)) { 85 | return this; 86 | } 87 | 88 | int objects = 0; 89 | int prims = 0; 90 | List> parameters = type.parameterList(); 91 | int length = parameters.size(); 92 | int[] reorder = new int[form.objects + form.prims]; 93 | MethodHandle[] cs = new MethodHandle[length]; 94 | for(int i = 0; i < length; i++) { 95 | Class parameter = parameters.get(i); 96 | int index = parameter.isPrimitive()? form.objects + prims++: objects++; 97 | MethodHandle c = components[index]; 98 | cs[i] = narrow(c, parameter); 99 | reorder[index] = i; 100 | } 101 | 102 | // need to fill the holes (objects & prims) for the constructor 103 | int hole = length; 104 | for(int i = objects; i < form.objects; i++) { 105 | reorder[i] = hole++; 106 | } 107 | for(int i = form.objects + prims; i < form.objects + form.prims; i++) { 108 | reorder[i] = hole++; 109 | } 110 | 111 | MethodType consType = type.changeReturnType(Object.class); 112 | MethodHandle cons = constructor; 113 | MethodType permutedType = erase(consType); 114 | if (objects < form.objects) { 115 | permutedType = permutedType.appendParameterTypes(classes(Object.class, form.objects - objects)); 116 | } 117 | if (prims < form.prims) { 118 | permutedType = permutedType.appendParameterTypes(classes(long.class, form.prims - prims)); 119 | } 120 | cons = permuteArguments(cons, permutedType, reorder); 121 | if (objects < form.objects) { 122 | cons = MethodHandles.insertArguments(cons, length, values(null, form.objects - objects)); 123 | } 124 | if (prims < form.prims) { 125 | cons = MethodHandles.insertArguments(cons, length, values(0L, form.prims - prims)); 126 | } 127 | cons = widen(cons, consType); 128 | 129 | return new TupleHandle(type, cons, cs); 130 | } 131 | 132 | private static final Map, MethodHandle> NARROWER_MAP; 133 | private static final Map, MethodHandle> WIDENER_MAP; 134 | static { 135 | MethodHandle longToDouble, intToFloat, doubleToLong, floatToInt; 136 | Lookup publicLookup = MethodHandles.publicLookup(); 137 | try { 138 | longToDouble = publicLookup.findStatic(Double.class, "longBitsToDouble", methodType(double.class, long.class)); 139 | intToFloat = publicLookup.findStatic(Float.class, "intBitsToFloat", methodType(float.class, int.class)); 140 | doubleToLong = publicLookup.findStatic(Double.class, "doubleToRawLongBits", methodType(long.class, double.class)); 141 | floatToInt = publicLookup.findStatic(Float.class, "floatToRawIntBits", methodType(int.class, float.class)); 142 | } catch (IllegalAccessException | NoSuchMethodException e) { 143 | throw (LinkageError)new LinkageError().initCause(e); 144 | } 145 | 146 | MethodHandle IDENTITY = identity(long.class); 147 | 148 | NARROWER_MAP = 149 | Map.of(boolean.class, explicitCastArguments(IDENTITY, methodType(boolean.class, long.class)), 150 | byte.class, explicitCastArguments(IDENTITY, methodType(byte.class, long.class)), 151 | short.class, explicitCastArguments(IDENTITY, methodType(short.class, long.class)), 152 | char.class, explicitCastArguments(IDENTITY, methodType(char.class, long.class)), 153 | int.class, explicitCastArguments(IDENTITY, methodType(int.class, long.class)), 154 | float.class, filterReturnValue(explicitCastArguments(IDENTITY, methodType(int.class, long.class)), intToFloat), 155 | double.class, longToDouble 156 | ); 157 | WIDENER_MAP = 158 | Map.of(boolean.class, explicitCastArguments(IDENTITY, methodType(long.class, boolean.class)), 159 | byte.class, IDENTITY.asType(methodType(long.class, byte.class)), 160 | short.class, IDENTITY.asType(methodType(long.class, short.class)), 161 | char.class, IDENTITY.asType(methodType(long.class, char.class)), 162 | int.class, IDENTITY.asType(methodType(long.class, int.class)), 163 | float.class, filterReturnValue(floatToInt, IDENTITY.asType(methodType(long.class, int.class))), 164 | double.class, doubleToLong 165 | ); 166 | } 167 | 168 | private static MethodHandle narrow(MethodHandle mh, Class type) { 169 | if (type == long.class || type == Object.class) { 170 | return mh; 171 | } 172 | if (!type.isPrimitive()) { 173 | return mh.asType(methodType(type, Object.class)); 174 | } 175 | return filterReturnValue(mh, NARROWER_MAP.get(type)); 176 | } 177 | private static MethodHandle widen(MethodHandle mh, MethodType type) { 178 | MethodHandle[] filters = new MethodHandle[type.parameterCount()]; 179 | Arrays.setAll(filters, i -> widener(type.parameterType(i))); 180 | return filterArguments(mh, 0, filters).asType(type); 181 | } 182 | private static MethodHandle widener(Class type) { 183 | if (type == long.class || type == Object.class) { 184 | return null; // no-op 185 | } 186 | if (!type.isPrimitive()) { 187 | return null; // no-op, will do a asType at the end 188 | } 189 | return WIDENER_MAP.get(type); 190 | } 191 | 192 | public static class Form { 193 | final int objects; 194 | final int prims; 195 | 196 | private Form(int objects, int prims) { 197 | this.objects = objects; 198 | this.prims = prims; 199 | } 200 | 201 | @Override 202 | public int hashCode() { 203 | return objects ^ prims; 204 | } 205 | 206 | @Override 207 | public boolean equals(Object o) { 208 | if (!(o instanceof Form)) { 209 | return false; 210 | } 211 | Form form = (Form)o; 212 | return objects == form.objects && prims == form.prims; 213 | } 214 | 215 | @Override 216 | public String toString() { 217 | return "Form " + objects + ':' + prims; 218 | } 219 | 220 | private Form accumulate(Class type) { 221 | return type.isPrimitive()? new Form(objects, prims + 1): new Form(objects + 1, prims); 222 | } 223 | 224 | public Form or(Form form) { 225 | return new Form(Math.max(objects, form.objects), Math.max(prims, form.prims)); 226 | } 227 | 228 | public Form and(Form form) { 229 | return new Form(objects + form.objects, prims + form.prims); 230 | } 231 | 232 | boolean isAssignableFrom(Form form) { 233 | return objects >= form.objects && prims >= form.prims; 234 | } 235 | 236 | MethodType asMethodType() { 237 | Class[] parameters = new Class[objects + prims]; 238 | for(int i = 0; i < objects; i++) { 239 | parameters[i] = Object.class; 240 | } 241 | for(int i = 0; i < prims; i++) { 242 | parameters[objects + i] = long.class; 243 | } 244 | return MethodType.methodType(void.class, parameters); 245 | } 246 | 247 | public TupleHandle createAs(MethodType type) { 248 | if (!isAssignableFrom(Form.of(type))) { 249 | throw new IllegalArgumentException("form " + this + " can not contains " + type); 250 | } 251 | return createImpl(this, type); 252 | } 253 | 254 | public static Form of(MethodType methodType) { 255 | if (methodType.returnType() != void.class) { 256 | throw new IllegalArgumentException(); 257 | } 258 | return methodType.parameterList().stream() 259 | .reduce(new Form(0, 0), Form::accumulate, (_1, _2) -> { throw new AssertionError(); }); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir.amber/module-info.java: -------------------------------------------------------------------------------- 1 | module fr.umlv.mjolnir.amber { 2 | requires fr.umlv.mjolnir; 3 | requires org.objectweb.asm; 4 | requires org.objectweb.asm.util; 5 | 6 | exports fr.umlv.mjolnir.amber; 7 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/AgentFacade.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir; 2 | 3 | public interface AgentFacade { 4 | void rewriteIfPossible(Class declaringClass); 5 | void addReads(Module source, Module destination); 6 | void addOpens(Module source, String packaze, Module destination); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/Mjolnir.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | import static java.lang.invoke.MethodHandles.dropArguments; 5 | import static java.lang.invoke.MethodHandles.lookup; 6 | import static java.lang.invoke.MethodType.methodType; 7 | 8 | import java.lang.StackWalker.Option; 9 | import java.lang.StackWalker.StackFrame; 10 | import java.lang.invoke.CallSite; 11 | import java.lang.invoke.MethodHandle; 12 | import java.lang.invoke.MethodHandles; 13 | import java.lang.invoke.MethodHandles.Lookup; 14 | import java.lang.invoke.MethodType; 15 | import java.lang.invoke.MutableCallSite; 16 | import java.util.Objects; 17 | import java.util.ServiceLoader; 18 | 19 | public class Mjolnir { 20 | static final AgentFacade AGENT_FACADE; 21 | static { 22 | AGENT_FACADE = ServiceLoader.load(AgentFacade.class).findFirst().orElse(null); 23 | } 24 | 25 | private static final ClassValue CONSTS_GET = getConstantValue(0); 26 | private static final ClassValue CONSTS_OVERRIDE = getConstantValue(1); 27 | 28 | static final Lookup PRIVATE_LOOKUP = MethodHandles.lookup(); 29 | static final StackWalker WALKER = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); 30 | static final ThreadLocal> BOOTSTRAP_LOCAL = new ThreadLocal<>(); 31 | static final ThreadLocal LOOKUP_LOCAL = new ThreadLocal<>(); 32 | 33 | private static ClassValue getConstantValue(int skipStacks) { 34 | return new ClassValue<>() { 35 | @Override 36 | protected Object computeValue(Class type) { 37 | /* de-activate to support ConstantDynamic call site (TODO re-evaluate) 38 | if (type.getDeclaredFields().length != 0) { 39 | throw new IllegalStateException("the bootstrap lambda should not capture any values"); 40 | }*/ 41 | 42 | Lookup lookup = LOOKUP_LOCAL.get(); 43 | try { 44 | if (lookup == null) { 45 | StackFrame frame = WALKER 46 | .walk(s -> s.filter(Mjolnir::filterOutMjolnirAndClassValueFrames).skip(skipStacks).findFirst()).get(); 47 | //System.out.println(frame.getDeclaringClass() + "." + frame.getMethodName() + " bci: " + frame.getByteCodeIndex()); 48 | Class declaringClass = frame.getDeclaringClass(); 49 | 50 | /*if (!Mjolnir.class.getModule().canRead(declaringClass.getModule())) { 51 | boolean rescue = true; 52 | if (AGENT_FACADE != null) { 53 | try { 54 | AGENT_FACADE.addReads(Mjolnir.class.getModule(), declaringClass.getModule()); 55 | AGENT_FACADE.addOpens(declaringClass.getModule(), declaringClass.getPackage().getName(), Mjolnir.class.getModule()); 56 | rescue = false; 57 | } catch(IllegalStateException e) { 58 | // do nothing 59 | } 60 | if (rescue) { 61 | Mjolnir.class.getModule().addReads(declaringClass.getModule()); 62 | } 63 | } 64 | }*/ 65 | Mjolnir.class.getModule().addReads(declaringClass.getModule()); 66 | lookup = MethodHandles.privateLookupIn(declaringClass, PRIVATE_LOOKUP); 67 | 68 | if (AGENT_FACADE != null) { 69 | try { 70 | AGENT_FACADE.rewriteIfPossible(declaringClass); 71 | } catch(IllegalStateException e) { 72 | System.err.println(e); 73 | } 74 | } 75 | } 76 | return BOOTSTRAP_LOCAL.get().bootstrap(lookup); 77 | } catch (Exception e) { 78 | e.printStackTrace(System.err); 79 | throw (LinkageError)new LinkageError().initCause(e); 80 | } 81 | } 82 | }; 83 | } 84 | 85 | static boolean filterOutMjolnirAndClassValueFrames(StackFrame f) { 86 | String className = f.getClassName(); 87 | return !className.equals("fr.umlv.mjolnir.Mjolnir") && 88 | !className.startsWith("java.lang.") && 89 | !className.startsWith("fr.umlv.mjolnir.Mjolnir$"); 90 | } 91 | 92 | public interface Bootstrap { 93 | public T bootstrap(Lookup lookup) throws Exception; 94 | } 95 | 96 | @SuppressWarnings("unchecked") 97 | public static T get(Bootstrap bootstrap) { 98 | // equivalent to a call to findConstant(null, bootstrap, CONSTS_GET, bootstrap); 99 | // inlined for perf 100 | 101 | BOOTSTRAP_LOCAL.set(bootstrap); 102 | try { 103 | return (T)CONSTS_GET.get(bootstrap.getClass()); 104 | } finally { 105 | BOOTSTRAP_LOCAL.set(null); // don't keep bootstrap for too long 106 | } 107 | } 108 | 109 | @SuppressWarnings("unchecked") 110 | public static T override(Object location, Bootstrap bootstrap) { 111 | // equivalent to a call to findConstant(null, location, CONSTS_OVERRIDE, bootstrap); 112 | // inlined for perf 113 | 114 | BOOTSTRAP_LOCAL.set(bootstrap); 115 | try { 116 | return (T)CONSTS_OVERRIDE.get(location.getClass()); 117 | } finally { 118 | BOOTSTRAP_LOCAL.set(null); // don't keep bootstrap for too long 119 | } 120 | } 121 | 122 | @SuppressWarnings("unchecked") 123 | static T findConstant(Lookup lookup, Object location, ClassValue constants, Bootstrap bootstrap) { 124 | Objects.requireNonNull(lookup); 125 | Objects.requireNonNull(location); 126 | Objects.requireNonNull(constants); 127 | Objects.requireNonNull(bootstrap); 128 | BOOTSTRAP_LOCAL.set(bootstrap); 129 | LOOKUP_LOCAL.set(lookup); 130 | try { 131 | return (T)constants.get(location.getClass()); 132 | } finally { 133 | BOOTSTRAP_LOCAL.set(null); // don't keep bootstrap for too long 134 | LOOKUP_LOCAL.set(null); // reset to null, expected by get() and override() 135 | } 136 | } 137 | 138 | private static class CS extends MutableCallSite { 139 | private static final MethodHandle INIT; 140 | static { 141 | try { 142 | INIT = lookup().findVirtual(CS.class, "init", methodType(Object.class, Lookup.class, Bootstrap.class)); 143 | } catch (NoSuchMethodException | IllegalAccessException e) { 144 | throw new AssertionError(e); 145 | } 146 | } 147 | 148 | private final ClassValue constants; 149 | 150 | public CS(Lookup lookup, ClassValue constants, MethodType type) { 151 | super(type); 152 | this.constants = constants; 153 | setTarget(INIT.bindTo(this).bindTo(lookup)); 154 | } 155 | 156 | @SuppressWarnings("unused") 157 | private T init(Lookup lookup, Bootstrap bootstrap) { 158 | T value = findConstant(lookup, bootstrap, constants, bootstrap); 159 | setTarget(dropArguments(constant(Object.class, value), 0, Bootstrap.class)); 160 | return value; 161 | } 162 | } 163 | 164 | public static CallSite bsm(Lookup lookup, String method, MethodType type) { 165 | return new CS(lookup, method.equals("get")? CONSTS_GET: CONSTS_OVERRIDE, type); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/OverrideEntryPoint.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Documented 10 | @Retention(RetentionPolicy.CLASS) 11 | @Target({ElementType.METHOD}) 12 | public @interface OverrideEntryPoint { 13 | // marker annotation 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/bytecode/AnnotationOracle.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.bytecode; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.UncheckedIOException; 6 | import java.util.HashMap; 7 | import java.util.HashSet; 8 | import java.util.Objects; 9 | import java.util.Optional; 10 | import java.util.function.Function; 11 | 12 | import org.objectweb.asm.AnnotationVisitor; 13 | import org.objectweb.asm.ClassReader; 14 | import org.objectweb.asm.ClassVisitor; 15 | import org.objectweb.asm.MethodVisitor; 16 | import org.objectweb.asm.Opcodes; 17 | 18 | import fr.umlv.mjolnir.OverrideEntryPoint; 19 | 20 | class AnnotationOracle { 21 | static final String OVERRIDE_ENTRY_POINT_NAME = 'L' + OverrideEntryPoint.class.getName().replace('.', '/') + ';'; 22 | 23 | private final HashMap> cache = new HashMap<>(); 24 | private final Function> classFileFinder; 25 | 26 | public AnnotationOracle(Function> classFileFinder) { 27 | this.classFileFinder = Objects.requireNonNull(classFileFinder); 28 | } 29 | 30 | public boolean isAnOverrideEntryPoint(String className, String methodName, String methodDescriptor) { 31 | return cache.computeIfAbsent(className, this::analyzeClass).contains(methodName + methodDescriptor); 32 | } 33 | 34 | private HashSet analyzeClass(String className) { 35 | HashSet set = new HashSet<>(); 36 | Optional classFileInputStream = classFileFinder.apply(className); 37 | if (!classFileInputStream.isPresent()) { 38 | return set; 39 | } 40 | try(InputStream input = classFileInputStream.get()) { 41 | ClassReader reader = new ClassReader(input); 42 | reader.accept(new ClassVisitor(Opcodes.ASM7) { 43 | @Override 44 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 45 | return new MethodVisitor(Opcodes.ASM6) { 46 | @Override 47 | public AnnotationVisitor visitAnnotation(String annotationDesc, boolean visible) { 48 | if (annotationDesc.equals(OVERRIDE_ENTRY_POINT_NAME)) { 49 | set.add(name + desc); 50 | } 51 | return null; 52 | } 53 | }; 54 | } 55 | }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); 56 | } catch (IOException e) { 57 | throw new UncheckedIOException(e); 58 | } 59 | return set; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/bytecode/Rewriter.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.bytecode; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.UncheckedIOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.nio.file.StandardCopyOption; 11 | import java.util.Collections; 12 | import java.util.Optional; 13 | import java.util.function.Function; 14 | import java.util.zip.ZipEntry; 15 | import java.util.zip.ZipFile; 16 | import java.util.zip.ZipOutputStream; 17 | 18 | import org.objectweb.asm.ClassReader; 19 | import org.objectweb.asm.ClassVisitor; 20 | import org.objectweb.asm.ClassWriter; 21 | import org.objectweb.asm.Handle; 22 | import org.objectweb.asm.MethodVisitor; 23 | import org.objectweb.asm.Opcodes; 24 | 25 | public class Rewriter { 26 | static final Handle BSM = new Handle(Opcodes.H_INVOKESTATIC, "fr/umlv/mjolnir/Mjolnir", "bsm", 27 | "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false); 28 | 29 | public static byte[] rewrite(InputStream input, Function> classFileFinder) throws IOException { 30 | AnnotationOracle annotationOracle = new AnnotationOracle(classFileFinder); 31 | ClassReader reader = new ClassReader(input); 32 | ClassWriter writer = new ClassWriter(reader, 0); 33 | reader.accept(new ClassVisitor(Opcodes.ASM7, writer) { 34 | @Override 35 | public MethodVisitor visitMethod(int access, String methodName, String methodDesc, String signature, String[] exceptions) { 36 | MethodVisitor mv = super.visitMethod(access, methodName, methodDesc, signature, exceptions); 37 | return new MethodVisitor(Opcodes.ASM7, mv) { 38 | @Override 39 | public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { 40 | // rewrite calls to Mjolnir.get() 41 | if (opcode == Opcodes.INVOKESTATIC && 42 | itf == false && 43 | owner.equals("fr/umlv/mjolnir/Mjolnir") && 44 | desc.equals("(Lfr/umlv/mjolnir/Mjolnir$Bootstrap;)Ljava/lang/Object;") && 45 | name.equals("get")) { 46 | super.visitInvokeDynamicInsn("get", desc, BSM); 47 | 48 | System.out.println("rewrite get: " + reader.getClassName() + '.' + methodName + methodDesc); 49 | return; 50 | } 51 | 52 | // rewrite calls to a method marked with @OverrideEntryPoint 53 | if (annotationOracle.isAnOverrideEntryPoint(owner, name, desc)) { 54 | String newDesc = desc; 55 | if (opcode != Opcodes.INVOKESTATIC) { 56 | newDesc = "(L" + owner + ';' + desc.substring(1); // prepend the receiver type 57 | } 58 | 59 | super.visitInvokeDynamicInsn("override", newDesc, BSM); 60 | System.out.println("rewrite override: " + reader.getClassName() + '.' + methodName + methodDesc); 61 | return; 62 | } 63 | 64 | super.visitMethodInsn(opcode, owner, name, desc, itf); 65 | } 66 | }; 67 | } 68 | }, 0); 69 | return writer.toByteArray(); 70 | } 71 | 72 | private static Optional findClassFile(ZipFile input, String className) { 73 | ZipEntry entry = new ZipEntry(className + ".class"); 74 | try { 75 | return Optional.ofNullable(input.getInputStream(entry)); 76 | } catch (IOException e) { 77 | throw new UncheckedIOException(e); 78 | } 79 | } 80 | 81 | public static void main(String[] args) throws IOException { 82 | Path path = Paths.get("target/test/artifact/test-fr.umlv.mjolnir-1.0.jar"); 83 | Path outputPath = Files.createTempFile("", ".jar"); 84 | 85 | System.out.println("rewrite " + path + " to " + outputPath); 86 | 87 | 88 | try(ZipFile input = new ZipFile(path.toFile()); 89 | OutputStream fileOutput = Files.newOutputStream(outputPath); 90 | ZipOutputStream jarOutput = new ZipOutputStream(fileOutput)) { 91 | Function> classFileFinder = className -> findClassFile(input, className); 92 | for(ZipEntry entry: Collections.list(input.entries())) { 93 | ZipEntry newEntry = new ZipEntry(entry.getName()); 94 | jarOutput.putNextEntry(newEntry); 95 | try(InputStream entryStream = input.getInputStream(entry)) { 96 | if (entry.getName().endsWith(".class")) { 97 | jarOutput.write(rewrite(entryStream, classFileFinder)); 98 | } else { 99 | entryStream.transferTo(jarOutput); 100 | } 101 | } 102 | } 103 | } 104 | 105 | System.out.println("move " + outputPath + " to " + path); 106 | 107 | Files.move(outputPath, path, StandardCopyOption.REPLACE_EXISTING); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/log/Log.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.log; 2 | 3 | import static java.lang.invoke.MethodHandles.empty; 4 | import static java.lang.invoke.MethodType.methodType; 5 | 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodHandles.Lookup; 9 | import java.lang.invoke.MutableCallSite; 10 | import java.lang.reflect.UndeclaredThrowableException; 11 | import java.util.function.Consumer; 12 | import java.util.function.Supplier; 13 | 14 | import fr.umlv.mjolnir.Mjolnir; 15 | import fr.umlv.mjolnir.OverrideEntryPoint; 16 | 17 | public class Log { 18 | public interface LogConfig { 19 | LogConfig enable(boolean enable); 20 | LogConfig outputer(Consumer outputer); 21 | void commit(); 22 | } 23 | 24 | static final class Info extends MutableCallSite { 25 | private boolean enable; 26 | private Consumer outputer; 27 | private final Object lock = new Object(); 28 | 29 | private static final MethodHandle DO_LOG; 30 | static { 31 | try { 32 | DO_LOG = MethodHandles.lookup().findStatic(Info.class, "doLog", methodType(void.class, Consumer.class, Supplier.class)); 33 | } catch (NoSuchMethodException | IllegalAccessException e) { 34 | throw new AssertionError(e); 35 | } 36 | } 37 | 38 | @SuppressWarnings("unused") 39 | private static void doLog(Consumer consumer, Supplier message) { 40 | consumer.accept(message.get()); 41 | } 42 | 43 | private static MethodHandle handle(boolean enable, Consumer outputer) { 44 | return (enable)? DO_LOG.bindTo(outputer): empty(methodType(void.class, Supplier.class)); 45 | } 46 | 47 | Info(boolean enable, Consumer outputer) { 48 | super(methodType(void.class, Supplier.class)); 49 | this.enable = enable; 50 | this.outputer = outputer; 51 | setTarget(handle(enable, outputer)); 52 | } 53 | 54 | void configure(boolean enable, Consumer outputer) { 55 | //System.out.println("configure " + enable + " " + outputer); 56 | synchronized(lock) { 57 | if (enable == this.enable && outputer == this.outputer) { 58 | return; 59 | } 60 | this.enable = enable; 61 | this.outputer = outputer; 62 | setTarget(handle(enable, outputer)); 63 | } 64 | } 65 | 66 | void populate(LogConfig config) { 67 | synchronized(lock) { 68 | config.enable(enable).outputer(outputer); 69 | } 70 | } 71 | } 72 | 73 | private static final ClassValue INFOS = new ClassValue<>() { 74 | @Override 75 | protected Info computeValue(Class arg0) { 76 | return new Info(true /* enable */, System.out::println); 77 | } 78 | }; 79 | 80 | public static LogConfig config(Class type) { 81 | Info info = INFOS.get(type); 82 | return new LogConfig() { 83 | private Consumer outputer; 84 | private boolean enable; 85 | 86 | { 87 | info.populate(this); 88 | } 89 | 90 | @Override 91 | public LogConfig outputer(Consumer outputer) { 92 | this.outputer = outputer; 93 | return this; 94 | } 95 | 96 | @Override 97 | public LogConfig enable(boolean enable) { 98 | this.enable = enable; 99 | return this; 100 | } 101 | 102 | @Override 103 | public void commit() { 104 | info.configure(enable, outputer); 105 | } 106 | }; 107 | } 108 | 109 | private static MethodHandle init(Lookup lookup) { 110 | //System.out.println("log init " + lookup.lookupClass()); 111 | return INFOS.get(lookup.lookupClass()).dynamicInvoker(); 112 | } 113 | 114 | @OverrideEntryPoint 115 | public static void log(Supplier message) { 116 | try { 117 | Mjolnir.override(message, Log::init).invokeExact(message); 118 | } catch(RuntimeException | Error e) { 119 | throw e; 120 | } catch (Throwable e) { 121 | throw new UndeclaredThrowableException(e); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/log/OldLog.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.log; 2 | 3 | import static java.lang.invoke.MethodType.methodType; 4 | 5 | import java.lang.StackWalker.StackFrame; 6 | import java.lang.invoke.CallSite; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodHandles.Lookup; 9 | import java.lang.invoke.StringConcatException; 10 | import java.lang.invoke.StringConcatFactory; 11 | import java.lang.reflect.UndeclaredThrowableException; 12 | import java.util.ArrayList; 13 | 14 | public class OldLog { 15 | public static void log(String format, Object... args) { 16 | StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); 17 | StackFrame frame = walker.walk(s -> s.skip(1).findFirst()).get(); 18 | Class declaringClass = frame.getDeclaringClass(); 19 | String location = declaringClass.getName() + '.' + frame.getMethodName() + '(' + frame.getFileName() + ':' + frame.getLineNumber() + ')'; 20 | 21 | ArrayList> types = new ArrayList<>(); 22 | StringBuilder builder = new StringBuilder(); 23 | 24 | for(int i = 0; i < format.length(); i++) { 25 | char c = format.charAt(i); 26 | if (c == '%') { 27 | switch(format.charAt(++i)) { 28 | case 's': 29 | types.add(String.class); 30 | builder.append('\u0001'); 31 | continue; 32 | case 'i': 33 | types.add(int.class); 34 | builder.append('\u0001'); 35 | continue; 36 | default: 37 | throw new IllegalArgumentException("invalid format " + format); 38 | } 39 | } else { 40 | builder.append(c); 41 | } 42 | } 43 | 44 | builder.append(" at ").append(location); 45 | String recipe = builder.toString(); 46 | 47 | 48 | Lookup lookup = MethodHandles.lookup(); 49 | CallSite callSite; 50 | try { 51 | callSite = StringConcatFactory.makeConcatWithConstants(lookup, "concat", methodType(String.class, types), recipe); 52 | } catch (StringConcatException e) { 53 | throw new AssertionError(e); 54 | } 55 | 56 | String message; 57 | try { 58 | message = (String)callSite.getTarget().invokeWithArguments(args); 59 | } catch (RuntimeException|Error e) { 60 | throw e; 61 | } catch(Throwable e) { 62 | throw new UndeclaredThrowableException(e); 63 | } 64 | 65 | 66 | 67 | System.out.println(message); 68 | } 69 | 70 | public static void main(String[] args) { 71 | log("foo"); 72 | log("foo %s bar %i", "toto", 3); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/util/stream/LoopBuilder.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.util.stream; 2 | 3 | import static java.lang.invoke.MethodHandles.dropArguments; 4 | import static java.lang.invoke.MethodHandles.zero; 5 | import static java.lang.invoke.MethodType.methodType; 6 | 7 | import java.lang.invoke.MethodHandle; 8 | import java.lang.invoke.MethodHandles; 9 | import java.lang.invoke.MethodHandles.Lookup; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import java.util.List; 14 | import java.util.function.Consumer; 15 | import java.util.function.Function; 16 | import java.util.function.Predicate; 17 | 18 | public class LoopBuilder { 19 | private final ArrayList clauses; 20 | private final ArrayList> types; 21 | 22 | LoopBuilder(ArrayList clauses, ArrayList> types) { 23 | this.clauses = clauses; 24 | this.types = types; 25 | } 26 | 27 | private static final MethodHandle ITERATOR, HAS_NEXT, NEXT, TEST, APPLY, ACCEPT; 28 | static { 29 | Lookup lookup = MethodHandles.publicLookup(); 30 | try { 31 | ITERATOR = lookup.findVirtual(Collection.class, "iterator", methodType(Iterator.class)); 32 | HAS_NEXT = lookup.findVirtual(Iterator.class, "hasNext", methodType(boolean.class)); 33 | NEXT = lookup.findVirtual(Iterator.class, "next", methodType(Object.class)); 34 | TEST = lookup.findVirtual(Predicate.class, "test", methodType(boolean.class, Object.class)); 35 | APPLY = lookup.findVirtual(Function.class, "apply", methodType(Object.class, Object.class)); 36 | ACCEPT = lookup.findVirtual(Consumer.class, "accept", methodType(void.class, Object.class)); 37 | } catch (NoSuchMethodException | IllegalAccessException e) { 38 | throw new AssertionError(); 39 | } 40 | } 41 | 42 | public static LoopBuilder create() { 43 | ArrayList clauses = new ArrayList<>(); 44 | ArrayList> types = new ArrayList<>(); 45 | 46 | clauses.add(new MethodHandle[] { ITERATOR, null, HAS_NEXT }); 47 | clauses.add(new MethodHandle[] { null, NEXT }); 48 | types.add(Iterator.class); 49 | 50 | return new LoopBuilder(clauses, types); 51 | } 52 | 53 | public void filter(Predicate predicate) { 54 | clauses.add(new MethodHandle[] { null, null, dropArguments(TEST.bindTo(predicate), 0, types), zero(void.class) }); 55 | } 56 | 57 | public void map(Function mapper) { 58 | clauses.add(new MethodHandle[] { null, dropArguments(APPLY.bindTo(mapper), 0, types) }); 59 | types.add(Object.class); 60 | } 61 | 62 | public MethodHandle forEach(Consumer consumer) { 63 | clauses.add(new MethodHandle[] { null, dropArguments(ACCEPT.bindTo(consumer), 0, types) }); 64 | 65 | return MethodHandles.loop(clauses.toArray(new MethodHandle[0][])); 66 | } 67 | 68 | public static void main(String[] args) throws Throwable { 69 | /* 70 | MethodHandle iterator, hasNext, next, test, apply, accept; 71 | Lookup lookup = MethodHandles.publicLookup(); 72 | try { 73 | iterator = lookup.findVirtual(Collection.class, "iterator", methodType(Iterator.class)); 74 | hasNext = lookup.findVirtual(Iterator.class, "hasNext", methodType(boolean.class)); 75 | next = lookup.findVirtual(Iterator.class, "next", methodType(Object.class)); 76 | test = lookup.findVirtual(Predicate.class, "test", methodType(boolean.class, Object.class)); 77 | apply = lookup.findVirtual(Function.class, "apply", methodType(Object.class, Object.class)); 78 | accept = lookup.findVirtual(Consumer.class, "accept", methodType(void.class, Object.class)); 79 | } catch (NoSuchMethodException | IllegalAccessException e) { 80 | throw new AssertionError(); 81 | } 82 | 83 | MethodHandle[] clause0 = new MethodHandle[] { iterator, null, hasNext }; 84 | MethodHandle[] clause1 = new MethodHandle[] { null, next }; 85 | MethodHandle[] clause2 = new MethodHandle[] { null, MethodHandles.dropArguments( 86 | apply.bindTo((Function)x -> x + " !"), 87 | 0, Iterator.class)}; 88 | MethodHandle[] clause3 = new MethodHandle[] { null, null, MethodHandles.dropArguments( 89 | test.bindTo(((Predicate)x -> true)), 90 | 0, Iterator.class, Object.class), 91 | MethodHandles.zero(void.class) }; 92 | MethodHandle[] clause4 = new MethodHandle[] { null, MethodHandles.dropArguments( 93 | accept.bindTo((Consumer)System.out::println), 94 | 0, Iterator.class, Object.class)}; 95 | 96 | MethodHandle loop = MethodHandles.loop(clause0, clause1, clause2, clause3, clause4); 97 | loop.invokeExact((Collection)List.of("a", "b")); 98 | */ 99 | 100 | LoopBuilder builder = LoopBuilder.create(); 101 | builder.map(x -> x + " !"); 102 | builder.filter(x -> true); 103 | MethodHandle loop = builder.forEach(System.out::println); 104 | loop.invokeExact((Collection)List.of("a", "b")); 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/fr/umlv/mjolnir/util/stream/Stream.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.util.stream; 2 | 3 | import java.lang.reflect.UndeclaredThrowableException; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.function.Predicate; 9 | 10 | public interface Stream { 11 | public Stream filter(Predicate predicate); 12 | public Stream map(Function mapper); 13 | public void forEach(Consumer consumer); 14 | 15 | public static Stream from(Collection collection) { 16 | return stream(collection, LoopBuilder.create()); 17 | } 18 | 19 | // should be private but Eclipse generates an accessor with an illegal visibility 20 | // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=518272 21 | /*private*/ static Stream stream(Collection collection, LoopBuilder builder) { 22 | return new Stream<>() { 23 | @Override 24 | public Stream filter(Predicate predicate) { 25 | builder.filter(predicate); 26 | return stream(collection, builder); 27 | } 28 | @Override 29 | public Stream map(Function mapper) { 30 | builder.map(mapper); 31 | return stream(collection, builder); 32 | } 33 | @Override 34 | public void forEach(Consumer consumer) { 35 | try { 36 | builder.forEach(consumer).invokeExact(collection); 37 | } catch(RuntimeException|Error e) { 38 | throw e; 39 | } catch (Throwable e) { 40 | throw new UndeclaredThrowableException(e); 41 | } 42 | } 43 | }; 44 | } 45 | 46 | public static void main(String[] args) { 47 | Stream.from(List.of("a", "b")).map(x -> x + "!").filter(x -> true).forEach(System.out::println); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/fr.umlv.mjolnir/module-info.java: -------------------------------------------------------------------------------- 1 | module fr.umlv.mjolnir { 2 | requires org.objectweb.asm; 3 | 4 | exports fr.umlv.mjolnir; 5 | exports fr.umlv.mjolnir.bytecode; 6 | 7 | uses fr.umlv.mjolnir.AgentFacade; 8 | } -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/PatternMatchingTests.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import static fr.umlv.mjolnir.Mjolnir.get; 4 | import static fr.umlv.mjolnir.amber.PatternMatchingMetaFactory.component; 5 | import static fr.umlv.mjolnir.amber.PatternMatchingMetaFactory.condy; 6 | import static fr.umlv.mjolnir.amber.PatternMatchingMetaFactory.indy; 7 | import static fr.umlv.mjolnir.amber.PatternMatchingMetaFactory.type; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | import java.lang.invoke.MethodHandle; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | import fr.umlv.mjolnir.Mjolnir.Bootstrap; 16 | 17 | @SuppressWarnings("static-method") 18 | public class PatternMatchingTests { 19 | public static class Point { 20 | private final int x; 21 | private final int y; 22 | 23 | public Point(int x, int y) { 24 | this.x = x; 25 | this.y = y; 26 | } 27 | 28 | @Deconstruct({ int.class, int.class }) 29 | public Object deconstructor(MethodHandle carrier) throws Throwable { 30 | if (x == y) { 31 | return carrier.invokeExact(false, 0, 0); // reject 32 | } 33 | return carrier.invokeExact(true, x, y); 34 | } 35 | } 36 | 37 | public static class User { 38 | private final String name; 39 | private final int age; 40 | 41 | public User(String message, int age) { 42 | this.name = message; 43 | this.age = age; 44 | } 45 | 46 | @Deconstruct({ String.class, int.class }) 47 | public Object deconstructor(MethodHandle carrier) throws Throwable { 48 | return carrier.invokeExact(true, name, age); 49 | } 50 | } 51 | 52 | private static String match(Object o) throws Throwable { 53 | Bootstrap condyLocation = lookup -> condy(lookup, Point.class, User.class); 54 | Object carrier = get(lookup -> indy(lookup, get(condyLocation))).invokeExact(o); 55 | switch((int)get(lookup -> component(lookup, type(), 0, get(condyLocation))).invokeExact(carrier)) { 56 | case 0: { 57 | int x = (int)get(lookup -> component(lookup, type(int.class, int.class), 2, get(condyLocation))).invokeExact(carrier); 58 | int y = (int)get(lookup -> component(lookup, type(int.class, int.class), 3, get(condyLocation))).invokeExact(carrier); 59 | return "Point " + x + ' ' + y; 60 | } 61 | case 1: { 62 | String name = (String)get(lookup -> component(lookup, type(String.class), 2, get(condyLocation))).invokeExact(carrier); 63 | return "User " + name; 64 | } 65 | default: 66 | return "no match"; 67 | } 68 | } 69 | 70 | @Test 71 | void test() throws Throwable { 72 | assertEquals("Point 99 747", match(new Point(99, 747))); 73 | assertEquals("User bob", match(new User("bob", 18))); 74 | 75 | assertEquals("no match", match(new Point(67, 67))); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir.amber/fr/umlv/mjolnir/amber/TupleHandleTests.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir.amber; 2 | 3 | import static java.lang.invoke.MethodType.methodType; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodType; 10 | import java.lang.invoke.MethodHandles.Lookup; 11 | import java.lang.invoke.StringConcatFactory; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | import fr.umlv.mjolnir.amber.TupleHandle.Form; 16 | 17 | @SuppressWarnings("static-method") 18 | public class TupleHandleTests { 19 | @Test 20 | void createSigInt() { 21 | TupleHandle handle = TupleHandle.create(int.class); 22 | assertEquals(methodType(void.class, int.class), handle.type()); 23 | } 24 | 25 | @Test 26 | void createSigString() { 27 | TupleHandle handle = TupleHandle.create(String.class); 28 | assertEquals(methodType(void.class, String.class), handle.type()); 29 | } 30 | 31 | @Test 32 | void createSigMixed() { 33 | TupleHandle handle = TupleHandle.create(String.class, int.class, boolean.class, String.class); 34 | assertEquals(methodType(void.class, String.class, int.class, boolean.class, String.class), handle.type()); 35 | } 36 | 37 | @Test 38 | void create() throws Throwable { 39 | TupleHandle handle = TupleHandle.create(String.class, int.class, boolean.class, String.class); 40 | Object object = handle.constructor().invokeExact("foo", 3, true, "bar"); 41 | assertEquals("foo", (String)handle.component(0).invokeExact(object)); 42 | assertEquals(3, (int)handle.component(1).invokeExact(object)); 43 | assertEquals(true, (boolean)handle.component(2).invokeExact(object)); 44 | assertEquals("bar", (String)handle.component(3).invokeExact(object)); 45 | } 46 | 47 | @Test 48 | void partial() throws Throwable { 49 | Form form = Form.of(methodType(void.class, int.class, boolean.class, String.class)); 50 | TupleHandle handle = form.createAs(methodType(void.class, int.class, boolean.class)); 51 | Object object = handle.constructor().invokeExact(3, true); 52 | assertEquals(3, (int)handle.component(0).invokeExact(object)); 53 | assertEquals(true, (boolean)handle.component(1).invokeExact(object)); 54 | } 55 | 56 | @Test 57 | void shareView() throws Throwable { 58 | try { 59 | MethodType type1 = methodType(void.class, int.class, double.class, Object.class); 60 | MethodType type2 = methodType(void.class, String.class, float.class, Object.class); 61 | Form form1 = Form.of(type1); 62 | Form form2 = Form.of(type2); 63 | Form form = form1.or(form2); 64 | TupleHandle handle1 = form.createAs(type1); 65 | TupleHandle handle2 = form.createAs(type2); 66 | 67 | Object object1 = handle1.constructor().invokeExact(7, 42.0, (Object)"hello"); 68 | Object object2 = handle2.constructor().invokeExact("fuzz", 4f, (Object)null); 69 | assertSame(object1.getClass(), object2.getClass()); 70 | } catch (Throwable e) { 71 | e.printStackTrace(); 72 | throw e; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir.amber/module-info.java: -------------------------------------------------------------------------------- 1 | module fr.umlv.mjolnir.amber { 2 | requires org.junit.jupiter.api; 3 | 4 | opens fr.umlv.mjolnir.amber; 5 | } -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir/fr/umlv/mjolnir/LogTests.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import fr.umlv.mjolnir.log.Log; 9 | 10 | @SuppressWarnings("static-method") 11 | public class LogTests { 12 | @Test 13 | void simple() throws Throwable { 14 | class Test1 { 15 | void m() { 16 | Log.log(() -> "simple"); 17 | } 18 | } 19 | boolean[] printed = new boolean[] { false }; 20 | Log.config(Test1.class).outputer(msg -> { 21 | assertEquals("simple", msg, "wrong message"); 22 | printed[0] = true; 23 | }).commit(); 24 | new Test1().m(); 25 | assertTrue(printed[0], "not printed"); 26 | } 27 | 28 | @Test 29 | void disable() throws Throwable { 30 | class Test2 { 31 | void m() { 32 | Log.log(() -> { fail("should not be called"); return null; }); 33 | } 34 | } 35 | Log.config(Test2.class).enable(false).commit(); 36 | new Test2().m(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir/fr/umlv/mjolnir/MjolnirTests.java: -------------------------------------------------------------------------------- 1 | package fr.umlv.mjolnir; 2 | 3 | import static java.lang.invoke.MethodType.methodType; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodHandles.Lookup; 10 | import java.lang.invoke.StringConcatFactory; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | @SuppressWarnings("static-method") 15 | public class MjolnirTests { 16 | @Test 17 | void lookup() throws Throwable { 18 | Mjolnir.get( 19 | lookup -> { 20 | assertSame(MjolnirTests.class, lookup.lookupClass()); 21 | assertTrue((lookup.lookupModes() & Lookup.PRIVATE) != 0); 22 | return null; 23 | }); 24 | } 25 | 26 | @Test 27 | void constant() throws Throwable { 28 | int value = Mjolnir.get(__ -> 42); 29 | assertEquals(42, value); 30 | } 31 | 32 | @Test 33 | void sum() throws Throwable { 34 | int value = (int)Mjolnir.get( 35 | lookup -> lookup.findStatic(Integer.class, "sum", methodType(int.class, int.class, int.class)) 36 | ).invokeExact(40, 2); 37 | assertEquals(42, value); 38 | } 39 | 40 | @Test 41 | void concat() throws Throwable { 42 | String s = (String)Mjolnir.get(lookup -> StringConcatFactory. 43 | makeConcatWithConstants(lookup, 44 | "concat", 45 | methodType(String.class, String.class), 46 | "Hello \u0001").dynamicInvoker()) 47 | .invokeExact("Mjolnir"); 48 | assertEquals("Hello Mjolnir", s); 49 | } 50 | 51 | private static int boostrapLine(Lookup lookup) { 52 | String className = lookup.lookupClass().getName(); 53 | int lineNumber = StackWalker.getInstance() 54 | .walk(s -> s.skip(1).filter(f -> f.getClassName().equals(className)).findFirst()) 55 | .get() 56 | .getLineNumber(); 57 | return lineNumber; 58 | } 59 | @Test 60 | void line() throws Throwable { 61 | int line = Mjolnir.get(MjolnirTests::boostrapLine); 62 | assertEquals(61, line); // this test may fail if you add more tests in front of this one ! 63 | } 64 | 65 | private static String boostrapFile(Lookup lookup) { 66 | String className = lookup.lookupClass().getName(); 67 | String filename = StackWalker.getInstance() 68 | .walk(s -> s.skip(1).filter(f -> f.getClassName().equals(className)).findFirst()) 69 | .get() 70 | .getFileName(); 71 | return filename; 72 | } 73 | @Test 74 | void filename() throws Throwable { 75 | String filename = Mjolnir.get(MjolnirTests::boostrapFile); 76 | assertEquals("MjolnirTests.java", filename); 77 | } 78 | 79 | @SuppressWarnings("unused") 80 | private static String hello(String name) { 81 | return "Hello " + name; 82 | } 83 | private static MethodHandle initHello(Lookup lookup) throws NoSuchMethodException, IllegalAccessException { 84 | return lookup.findStatic(lookup.lookupClass(), "hello", methodType(String.class, String.class)); 85 | } 86 | @Test 87 | void hello() throws Throwable { 88 | String result = (String)Mjolnir.get(MjolnirTests::initHello).invokeExact("Mjolnir"); 89 | assertEquals("Hello Mjolnir", result); 90 | } 91 | 92 | @SuppressWarnings("unused") 93 | private static int incr(int value) { 94 | return value + 1; 95 | } 96 | private static MethodHandle init(Lookup lookup) throws NoSuchMethodException, IllegalAccessException { 97 | return lookup.findStatic(lookup.lookupClass(), "incr", methodType(int.class, int.class)); 98 | } 99 | @Test 100 | void loop() throws Throwable { 101 | int i = 0; 102 | while (i < 10_000_000) { 103 | i = (int)Mjolnir.get(MjolnirTests::init).invokeExact(i); 104 | } 105 | assertEquals(10_000_000, i); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/fr.umlv.mjolnir/module-info.java: -------------------------------------------------------------------------------- 1 | open module fr.umlv.mjolnir { 2 | requires org.junit.jupiter.api; 3 | } --------------------------------------------------------------------------------