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 
  56     //
  57     final static String defaultPkg = "jextract.dump";
  58     private static Context instance = new Context();
  59     public final Logger logger = Logger.getLogger(getClass().getPackage().getName());
  60 
  61     private Context() {
  62         pkgMap = new HashMap<>();
  63         headerMap = new HashMap<>();
  64         clangArgs = new ArrayList<>();
  65         sources = new TreeSet<>();
  66         libraries = new ArrayList<>();

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