1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.File;
  25 import java.io.IOException;
  26 import java.nio.file.Files;
  27 import java.nio.file.Path;
  28 import java.nio.file.Paths;
  29 import java.util.ArrayList;
  30 import java.util.Arrays;
  31 import java.util.List;
  32 import java.util.concurrent.TimeUnit;
  33 
  34 /*
  35  * @test
  36  * @modules jdk.jextract
  37  * @run main TestJextractFFI
  38  */
  39 public class TestJextractFFI {
  40     final String jextract_cmd;
  41     final String javac_cmd;
  42     final Path jclang_src_path;
  43     final Path clang_header_path;
  44     final Path clang_lib_path;
  45 
  46     final static String CLANG_JAR = "clang.jar";
  47     final static String CLANG_JNI_PATH = "clang_jni";
  48     final static String CLANG_FFI_PATH = "clang_ffi";
  49     final static String JCLANG_PATH = "jclang";
  50 
  51     TestJextractFFI(Path clang_header_path, Path clang_lib_path, Path jclang_src) {
  52         this.clang_header_path = clang_header_path;
  53         this.clang_lib_path = clang_lib_path;
  54         this.jclang_src_path = jclang_src;
  55 
  56         final Path jdk_path = Paths.get(System.getProperty("test.jdk"))
  57                 .resolve("bin").toAbsolutePath();
  58         jextract_cmd = jdk_path.resolve("jextract").toString();
  59         javac_cmd = jdk_path.resolve("javac").toString();
  60     }
  61 
  62     private Path getClassPath(String component) {
  63         return Paths.get(System.getProperty("test.classes")).resolve(component);
  64     }
  65 
  66     File launch(List<String> command) throws IOException, InterruptedException {
  67         File err = File.createTempFile(
  68                 Paths.get(command.get(0)).getFileName().toString(), null);
  69         err.deleteOnExit();
  70         Process proc = new ProcessBuilder()
  71                 .directory(new File(System.getProperty("test.classes")))
  72                 .command(command)
  73                 .redirectError(err)
  74                 .start();
  75         String cmdline = proc.info().commandLine().orElse(String.join(" ", command));
  76         proc.waitFor(3, TimeUnit.MINUTES);
  77         Files.lines(err.toPath()).forEachOrdered(System.err::println);
  78         if (proc.exitValue() != 0) {
  79             throw new Error("Command '" + cmdline +
  80                 "' exit with non-zero value: " + proc.exitValue());
  81         }
  82         return err;
  83     }
  84 
  85     public void jextractJNI() throws IOException, InterruptedException {
  86         List<String> command = List.of(jextract_cmd,
  87                 "-I", clang_header_path.toString(),
  88                 "-t", "clang",
  89                 "-o", CLANG_JAR,
  90                 "-d", CLANG_JNI_PATH,
  91                 clang_header_path
  92                         .resolve("clang-c").resolve("Index.h").toString());
  93         launch(command);
  94     }
  95 
  96     public void buildJclang() throws IOException, InterruptedException {
  97         List<String> command = new ArrayList<>(List.of(
  98                 javac_cmd,
  99                 "--module-path", CLANG_JAR,
 100                 "-d", JCLANG_PATH));
 101         Files.walk(jclang_src_path)
 102                 .map(path -> path.toString())
 103                 .filter(filename -> filename.endsWith(".java"))
 104                 .forEach(command::add);
 105         launch(command);
 106     }
 107 
 108     public void jextractFFI() throws IOException, InterruptedException {
 109         List<String> command = List.of(jextract_cmd,
 110                 "-I", clang_header_path.toString(),
 111                 "-t", "clang",
 112                 "-d", CLANG_FFI_PATH,
 113                 "-J-Djextract.debug=true",
 114                 "-J-Dlibclang.debug=true",
 115                 "-J-Djava.library.path=" + clang_lib_path.toString(),
 116                 "-J--module-path", "-J" + CLANG_JAR,
 117                 "-J--upgrade-module-path", "-J" + JCLANG_PATH,
 118                 clang_header_path
 119                         .resolve("clang-c").resolve("Index.h").toString());
 120         File err = launch(command);
 121         if (! Files.lines(err.toPath())
 122                 .anyMatch(s -> s.contains("FFI"))) {
 123             throw new Error("Not running jextract on FFI stack");
 124         }
 125     }
 126 
 127     public void compareResult() throws IOException {
 128         // We cannot do jar file byte-to-byte comparison because
 129         // jar file format include timestamp regarding modification,
 130         // thus such comparison will fail.
 131         Path[] jni = Files.walk(getClassPath(CLANG_JNI_PATH))
 132                 .sorted().toArray(Path[]::new);
 133         Path[] ffi = Files.walk(getClassPath(CLANG_FFI_PATH))
 134                 .sorted().toArray(Path[]::new);
 135         if (jni.length != ffi.length) {
 136             throw new Error("Number of classes mismatch.");
 137         }
 138 
 139         // Skip the first element which is top folder
 140         for (int i = 1; i < jni.length; i++) {
 141             final Path pathJNI = jni[i];
 142             final Path pathFFI = ffi[i];
 143             // Basename should be the same
 144             if (! pathJNI.getFileName().equals(pathFFI.getFileName())) {
 145                 throw new Error("Different filename:" + pathJNI.getFileName()
 146                         + " vs " + pathFFI.getFileName());
 147             }
 148             // Direcoty for package
 149             if (Files.isDirectory(pathJNI)) {
 150                 if (Files.isDirectory(pathFFI)) {
 151                     continue;
 152                 }
 153                 throw new Error("Mismatch file type: " + pathJNI.getFileName()
 154                         + " vs " + pathFFI.getFileName());
 155             }
 156             // ignore non-class files
 157             if (!pathJNI.getFileName().endsWith("class")) {
 158                 continue;
 159             }
 160             // Classfile
 161             if (! Arrays.equals(
 162                     Files.readAllBytes(pathJNI),
 163                     Files.readAllBytes(pathFFI))) {
 164                 throw new Error("Mismatch class files: " + pathJNI.toString()
 165                         + " vs " + pathFFI.toString());
 166             }
 167         }
 168     }
 169 
 170     public void run() throws IOException, InterruptedException {
 171         jextractJNI();
 172         buildJclang();
 173         jextractFFI();
 174         compareResult();
 175     }
 176 
 177     public static int main(String... args) throws IOException, InterruptedException {
 178         final Path srcPath = Paths.get(System.getProperty("test.src"));
 179         final String clangInclude = System.getProperty("clang.include.path");
 180         final String clangLib = System.getProperty("clang.lib.path");
 181 
 182         if (clangInclude != null && clangLib != null) {
 183             TestJextractFFI test = new TestJextractFFI(
 184                     Paths.get(clangInclude).toAbsolutePath(),
 185                     Paths.get(clangLib).toAbsolutePath(),
 186                     srcPath.resolve("src").toAbsolutePath()
 187             );
 188 
 189             test.run();
 190         } else {
 191             // FIXME: we should try to figure out clang paths automatically.
 192             System.err.println("WARNING: clang paths not found, vacuously passing");
 193         }
 194         return 0;
 195     }
 196 }