1 /*
   2  * Copyright (c) 2015, 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 package com.sun.tools.jextract;
  24 
  25 import jdk.internal.clang.*;
  26 
  27 import java.io.File;
  28 import java.io.IOException;
  29 import java.io.OutputStream;
  30 import java.io.UncheckedIOException;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.util.*;
  34 import java.util.function.Function;
  35 import java.util.jar.JarOutputStream;
  36 import java.util.logging.Logger;
  37 import java.util.zip.ZipEntry;
  38 
  39 import static java.nio.file.StandardOpenOption.*;
  40 
  41 /**
  42  * The setup for the tool execution
  43  */
  44 public class Context {
  45     // The folder path mapping to package name
  46     private final Map<Path, String> pkgMap;
  47     // The header file parsed
  48     private final Map<Path, HeaderFile> headerMap;
  49     // The args for parsing C
  50     final List<String> clangArgs;
  51     // The set of source header files
  52     final Set<Path>  sources;
  53     // The list of libraries
  54     final List<String> libraries;
  55     // The list of library paths
  56     final List<String> libraryPaths;
  57 
  58     //
  59     final static String defaultPkg = "jextract.dump";
  60     private static Context instance = new Context();
  61     public final Logger logger = Logger.getLogger(getClass().getPackage().getName());
  62 
  63     private Context() {
  64         pkgMap = new HashMap<>();
  65         headerMap = new HashMap<>();
  66         clangArgs = new ArrayList<>();
  67         sources = new TreeSet<>();
  68         libraries = new ArrayList<>();
  69         libraryPaths = new ArrayList<>();
  70     }
  71 
  72     // used only for jtreg testing
  73     public static Context newInstance() {
  74         return instance = new Context();
  75     }
  76 
  77     public static Context getInstance() {
  78         return instance;
  79     }
  80 
  81     public void addSource(Path path) {
  82         sources.add(path);
  83     }
  84 
  85     /**
  86      * Setup a package name for a given folder.
  87      *
  88      * @param folder The path to the folder, use null to set catch-all.
  89      * @param pkg    The package name
  90      * @return True if the folder is setup successfully. False is a package
  91      * has been assigned for the folder.
  92      */
  93     public boolean usePackageForFolder(Path folder, String pkg) {
  94         if (folder != null) {
  95             folder = folder.toAbsolutePath();
  96             if (!Files.isDirectory(folder)) {
  97                 folder = folder.getParent();
  98             }
  99         }
 100         String existing = pkgMap.putIfAbsent(folder, pkg);
 101         final String finalFolder = (null == folder) ? "all folders not configured" : folder.toString();
 102         if (null == existing) {
 103             logger.config(() -> "Package " + pkg + " is selected for " + finalFolder);
 104             return true;
 105         } else {
 106             logger.warning(() -> "Package " + existing + " had been selected for " + finalFolder + ", request to use " + pkg + " is ignored.");
 107             return false;
 108         }
 109     }
 110 
 111     public static class Entity {
 112         public final String pkg;
 113         public final String entity;
 114 
 115         Entity(String pkg, String entity) {
 116             this.pkg = pkg;
 117             this.entity = entity;
 118         }
 119     }
 120 
 121     /**
 122      * Determine package and interface name given a path. If the path is
 123      * a folder, then only package name is determined. The package name is
 124      * determined with the longest path matching the setup. If the path is not
 125      * setup for any package, the default package name is returned.
 126      *
 127      * @param origin The source path
 128      * @return The Entity
 129      * @see Context::usePackageForFolder(Path, String)
 130      */
 131     public Entity whatis(Path origin) {
 132         // normalize to absolute path
 133         origin = origin.toAbsolutePath();
 134         String filename = null;
 135         if (!Files.isDirectory(origin)) {
 136             // ensure it's a folder name
 137             filename = origin.getFileName().toString();
 138             origin = origin.getParent();
 139         }
 140         Path path = origin;
 141 
 142         // search the map for a hit with longest path
 143         while (path != null && !pkgMap.containsKey(path)) {
 144             path = path.getParent();
 145         }
 146 
 147         int start;
 148         String pkg;
 149         if (path != null) {
 150             start = path.getNameCount();
 151             pkg = pkgMap.get(path);
 152         } else {
 153             pkg = pkgMap.get(null);
 154             if (pkg == null) {
 155                 start = 0;
 156                 pkg = defaultPkg;
 157             } else {
 158                 start = origin.getNameCount();
 159             }
 160         }
 161 
 162         if (filename == null) {
 163             // a folder, only pkg name matters
 164             return new Entity(pkg, null);
 165         }
 166 
 167         StringBuilder sb = new StringBuilder();
 168         while (start < origin.getNameCount()) {
 169             sb.append(Utils.toJavaIdentifier(origin.getName(start++).toString()));
 170             sb.append("_");
 171         }
 172 
 173         int ext = filename.lastIndexOf('.');
 174         if (ext != -1) {
 175             sb.append(filename.substring(0, ext));
 176         } else {
 177             sb.append(filename);
 178         }
 179         return new Entity(pkg, Utils.toClassName(sb.toString()));
 180     }
 181 
 182     HeaderFile getHeaderFile(Path header, HeaderFile main) {
 183         if (!Files.isRegularFile(header)) {
 184             logger.warning(() -> "Not a regular file: " + header.toString());
 185             throw new IllegalArgumentException(header.toString());
 186         }
 187 
 188         final Context.Entity e = whatis(header);
 189         return new HeaderFile(header, e.pkg, e.entity, main);
 190     }
 191 
 192     void processCursor(Cursor c, HeaderFile main, Function<HeaderFile, CodeFactory> fn) {
 193         SourceLocation loc = c.getSourceLocation();
 194         if (loc == null) {
 195             logger.info(() -> "Ignore Cursor " + c.spelling() + "@" + c.USR() + " has no SourceLocation");
 196             return;
 197         }
 198         logger.fine(() -> "Do cursor: " + c.spelling() + "@" + c.USR());
 199 
 200         HeaderFile header;
 201         boolean isBuiltIn = false;
 202 
 203         if (loc.isFromMainFile()) {
 204             header = main;
 205         } else {
 206             SourceLocation.Location src = loc.getFileLocation();
 207             if (src == null) {
 208                 logger.info(() -> "Cursor " + c.spelling() + "@" + c.USR() + " has no FileLocation");
 209                 return;
 210             }
 211 
 212             Path p = src.path();
 213             if (p == null) {
 214                 logger.fine(() -> "Found built-in type: " + c.spelling());
 215                 header = main;
 216                 isBuiltIn = true;
 217             } else {
 218                 p = p.normalize().toAbsolutePath();
 219                 header = headerMap.get(p);
 220                 if (header == null) {
 221                     final HeaderFile hf = header = getHeaderFile(p, main);
 222                     logger.config(() -> "First encounter of header file " + hf.path + ", assigned to package " + hf.pkgName);
 223                     // Only generate code for header files sepcified or in the same package
 224                     // System headers are excluded, they need to be explicitly specified in jextract cmdline
 225                     if (sources.contains(p) ||
 226                         (!loc.isInSystemHeader()) && (header.pkgName.equals(main.pkgName))) {
 227                         logger.config("Code gen for header " + p + " enabled in package " + header.pkgName);
 228                         header.useCodeFactory(fn.apply(header));
 229                     }
 230                     headerMap.put(p, header);
 231                 }
 232             }
 233         }
 234 
 235         header.processCursor(c, main, isBuiltIn);
 236     }
 237 
 238     public void parse(Function<HeaderFile, CodeFactory> fn) {
 239         sources.forEach(path -> {
 240             if (headerMap.containsKey(path)) {
 241                 logger.info(() -> path.toString() + " seen earlier via #include");
 242                 return;
 243             }
 244 
 245             HeaderFile hf = headerMap.computeIfAbsent(path, p -> getHeaderFile(p, null));
 246             hf.useLibraries(libraries, libraryPaths);
 247             hf.useCodeFactory(fn.apply(hf));
 248             logger.info(() -> "Parsing header file " + path);
 249 
 250             Index index = LibClang.createIndex();
 251             Cursor tuCursor = index.parse(path.toString(),
 252                     d -> {
 253                         System.err.println(d);
 254                         if (d.severity() >  Diagnostic.CXDiagnostic_Warning) {
 255                             throw new RuntimeException(d.toString());
 256                         }
 257                     },
 258                     Main.INCLUDE_MACROS,
 259                     clangArgs.toArray(new String[0]));
 260 
 261             tuCursor.children()
 262                     .peek(c -> logger.finest(
 263                         () -> "Cursor: " + c.spelling() + "@" + c.USR() + "?" + c.isDeclaration()))
 264                     .filter(c -> c.isDeclaration() || c.isPreprocessing())
 265                     .forEach(c -> processCursor(c, hf, fn));
 266         });
 267     }
 268 
 269     private Map<String, List<CodeFactory>> getPkgCfMap() {
 270         final Map<String, List<CodeFactory>> mapPkgCf = new HashMap<>();
 271         // Build the pkg to CodeFactory map
 272         headerMap.values().forEach(header -> {
 273             CodeFactory cf = header.cf;
 274             String pkg = header.pkgName;
 275             logger.config(() -> "File " + header + " is in package: " + pkg);
 276             if (cf == null) {
 277                 logger.config(() -> "File " + header + " code generation is not activated!");
 278                 return;
 279             }
 280             List<CodeFactory> l = mapPkgCf.computeIfAbsent(pkg, k -> new ArrayList<>());
 281             l.add(cf);
 282             logger.config(() -> "Add cf " + cf + " to pkg " + pkg + ", size is now " + l.size());
 283         });
 284         return Collections.unmodifiableMap(mapPkgCf);
 285     }
 286 
 287     public Map<String, byte[]> collectClasses(String... pkgs) {
 288         final Map<String, byte[]> rv = new HashMap<>();
 289         final Map<String, List<CodeFactory>> mapPkgCf = getPkgCfMap();
 290         for (String pkg_name : pkgs) {
 291             mapPkgCf.getOrDefault(pkg_name, Collections.emptyList())
 292                     .forEach(cf -> rv.putAll(cf.collect()));
 293         }
 294         return Collections.unmodifiableMap(rv);
 295     }
 296 
 297     private void writeJar(CodeFactory cf, JarOutputStream jar) {
 298         cf.collect().entrySet().stream().forEach(e -> {
 299             try {
 300                 String path = e.getKey().replace('.', File.separatorChar) + ".class";
 301                 logger.fine(() -> "Add " + path);
 302                 jar.putNextEntry(new ZipEntry(path));
 303                 jar.write(e.getValue());
 304                 jar.closeEntry();
 305             } catch (IOException ioe) {
 306                 throw new UncheckedIOException(ioe);
 307             }
 308         });
 309     }
 310 
 311     public void collectJarFile(final JarOutputStream jos, String... pkgs) {
 312         final Map<String, List<CodeFactory>> mapPkgCf = getPkgCfMap();
 313 
 314         for (String pkg_name : pkgs) {
 315             // convert '.' to '/' to use as a path
 316             String entryName = Utils.toInternalName(pkg_name, "");
 317             // package folder
 318             if (!entryName.isEmpty()) {
 319                 try {
 320                     jos.putNextEntry(new ZipEntry(entryName));
 321                 } catch (IOException ex) {
 322                     throw new UncheckedIOException(ex);
 323                 }
 324             }
 325             logger.fine(() -> "Produce for package " + pkg_name);
 326             mapPkgCf.getOrDefault(pkg_name, Collections.emptyList())
 327                     .forEach(cf -> writeJar(cf, jos));
 328         }
 329     }
 330 
 331     public void collectJarFile(final Path jar, String... pkgs) throws IOException {
 332         logger.info(() -> "Collecting jar file " + jar);
 333         try (OutputStream os = Files.newOutputStream(jar, CREATE, TRUNCATE_EXISTING, WRITE);
 334              JarOutputStream jo = new JarOutputStream(os)) {
 335             collectJarFile(jo, pkgs);
 336         } catch (UncheckedIOException uioe) {
 337             throw uioe.getCause();
 338         }
 339     }
 340 
 341     /**
 342      * Perform a local lookup, any undefined type will cause a JType
 343      * be defined within origin scope.
 344      *
 345      * @param type   The libclang type
 346      * @param origin The path of the file where type is encountered
 347      * @return The JType
 348      */
 349     JType getJType(final Type type, Path origin) {
 350         Path p = origin.normalize().toAbsolutePath();
 351 
 352         HeaderFile hf = headerMap.get(p);
 353         // We should not encounter a type if the header file reference to it is not yet processed
 354         assert(null != hf);
 355         if (hf == null) {
 356             throw new IllegalArgumentException("Failed to lookup header for " + p + " (origin: " + origin + ")");
 357         }
 358 
 359         return hf.localLookup(type);
 360     }
 361 
 362     /**
 363      * Perform a global lookup
 364      *
 365      * @param c The cursor define or declare the type.
 366      * @return
 367      */
 368     public JType getJType(final Cursor c) {
 369         if (c.isInvalid()) {
 370             throw new IllegalArgumentException();
 371         }
 372         SourceLocation loc = c.getSourceLocation();
 373         if (null == loc) {
 374             return null;
 375         }
 376         Path p = loc.getFileLocation().path();
 377         if (null == p) {
 378             return null;
 379         }
 380         return getJType(c.type(), p);
 381     }
 382 }