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 import jdk.internal.foreign.LibrariesHelper;
  27 
  28 import java.io.ByteArrayOutputStream;
  29 import java.io.File;
  30 import java.io.IOException;
  31 import java.io.OutputStream;
  32 import java.io.PrintWriter;
  33 import java.io.UncheckedIOException;
  34 import java.lang.invoke.MethodHandles;
  35 import java.foreign.Library;
  36 import java.nio.file.Files;
  37 import java.nio.file.Path;
  38 import java.util.ArrayList;
  39 import java.util.Arrays;
  40 import java.util.Collections;
  41 import java.util.HashMap;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Properties;
  45 import java.util.Set;
  46 import java.util.TreeSet;
  47 import java.util.function.Function;
  48 import java.util.function.Predicate;
  49 import java.util.jar.JarOutputStream;
  50 import java.util.logging.Logger;
  51 import java.util.regex.Pattern;
  52 import java.util.stream.Collectors;
  53 import java.util.zip.ZipEntry;
  54 import com.sun.tools.jextract.parser.Parser;
  55 import com.sun.tools.jextract.tree.FunctionTree;
  56 import com.sun.tools.jextract.tree.HeaderTree;
  57 import com.sun.tools.jextract.tree.Tree;
  58 
  59 import static java.nio.file.StandardOpenOption.CREATE;
  60 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
  61 import static java.nio.file.StandardOpenOption.WRITE;
  62 
  63 /**
  64  * The setup for the tool execution
  65  */
  66 public final class Context {
  67     // package name to TypeDictionary
  68     private final Map<String, TypeDictionary> tdMap;
  69     // The folder path mapping to package name
  70     private final Map<Path, String> pkgMap;
  71     // The header file parsed
  72     private final Map<Path, HeaderFile> headerMap;
  73     // The args for parsing C
  74     private final List<String> clangArgs;
  75     // The set of source header files
  76     private final Set<Path>  sources;
  77     // The list of library names
  78     private final List<String> libraryNames;
  79     // The list of library paths
  80     private final List<String> libraryPaths;
  81     // The list of library paths for link checks
  82     private final List<String> linkCheckPaths;
  83     // Symbol patterns to be excluded
  84     private final List<Pattern> excludeSymbols;
  85 
  86     final PrintWriter out;
  87     final PrintWriter err;
  88 
  89     private Predicate<String> symChecker;
  90     private Predicate<String> symFilter;
  91 
  92     private final Parser parser;
  93 
  94     private final static String defaultPkg = "jextract.dump";
  95     final Logger logger = Logger.getLogger(getClass().getPackage().getName());
  96 
  97     public Context(PrintWriter out, PrintWriter err) {
  98         this.tdMap = new HashMap<>();
  99         this.pkgMap = new HashMap<>();
 100         this.headerMap = new HashMap<>();
 101         this.clangArgs = new ArrayList<>();
 102         this.sources = new TreeSet<>();
 103         this.libraryNames = new ArrayList<>();
 104         this.libraryPaths = new ArrayList<>();
 105         this.linkCheckPaths = new ArrayList<>();
 106         this.excludeSymbols = new ArrayList<>();
 107         this.parser = new Parser(out, err, Main.INCLUDE_MACROS);
 108         this.out = out;
 109         this.err = err;
 110     }
 111 
 112     public Context() {
 113         this(new PrintWriter(System.out, true), new PrintWriter(System.err, true));
 114     }
 115 
 116     TypeDictionary typeDictionaryFor(String pkg) {
 117         return tdMap.computeIfAbsent(pkg, p->new TypeDictionary(this, p));
 118     }
 119 
 120     void addClangArg(String arg) {
 121         clangArgs.add(arg);
 122     }
 123 
 124     public void addSource(Path path) {
 125         sources.add(path);
 126     }
 127 
 128     void addLibraryName(String name) {
 129         libraryNames.add(name);
 130     }
 131 
 132     void addLibraryPath(String path) {
 133         libraryPaths.add(path);
 134     }
 135 
 136     void addLinkCheckPath(String path) {
 137         linkCheckPaths.add(path);
 138     }
 139 
 140     void addExcludeSymbols(String pattern) {
 141         excludeSymbols.add(Pattern.compile(pattern));
 142     }
 143 
 144     private void initSymChecker() {
 145         if (!libraryNames.isEmpty() && !linkCheckPaths.isEmpty()) {
 146             Library[] libs = LibrariesHelper.loadLibraries(MethodHandles.lookup(),
 147                 linkCheckPaths.toArray(new String[0]),
 148                 libraryNames.toArray(new String[0]));
 149 
 150             // check if the given symbol is found in any of the libraries or not.
 151             // If not found, warn the user for the missing symbol.
 152             symChecker = name -> {
 153                 if (Main.DEBUG) {
 154                     err.println("Searching symbol: " + name);
 155                 }
 156                 return (Arrays.stream(libs).filter(lib -> {
 157                         try {
 158                             lib.lookup(name);
 159                             if (Main.DEBUG) {
 160                                 err.println("Found symbol: " + name);
 161                             }
 162                             return true;
 163                         } catch (NoSuchMethodException nsme) {
 164                             return false;
 165                         }
 166                     }).findFirst().isPresent());
 167             };
 168         } else {
 169             symChecker = null;
 170         }
 171     }
 172 
 173     private boolean isSymbolFound(String name) {
 174         return symChecker == null? true : symChecker.test(name);
 175     }
 176 
 177     private void initSymFilter() {
 178         if (!excludeSymbols.isEmpty()) {
 179             Pattern[] pats = excludeSymbols.toArray(new Pattern[0]);
 180             symFilter = name -> {
 181                 return Arrays.stream(pats).filter(pat -> pat.matcher(name).matches()).
 182                     findFirst().isPresent();
 183             };
 184         } else {
 185             symFilter = null;
 186         }
 187     }
 188 
 189     private boolean isSymbolExcluded(String name) {
 190         return symFilter == null? false : symFilter.test(name);
 191     }
 192 
 193     /**
 194      * Setup a package name for a given folder.
 195      *
 196      * @param folder The path to the folder, use null to set catch-all.
 197      * @param pkg    The package name
 198      * @return True if the folder is setup successfully. False is a package
 199      * has been assigned for the folder.
 200      */
 201     public boolean usePackageForFolder(Path folder, String pkg) {
 202         if (folder != null) {
 203             folder = folder.toAbsolutePath();
 204             if (!Files.isDirectory(folder)) {
 205                 folder = folder.getParent();
 206             }
 207         }
 208         String existing = pkgMap.putIfAbsent(folder, pkg);
 209         final String finalFolder = (null == folder) ? "all folders not configured" : folder.toString();
 210         if (null == existing) {
 211             logger.config(() -> "Package " + pkg + " is selected for " + finalFolder);
 212             return true;
 213         } else {
 214             logger.warning(() -> "Package " + existing + " had been selected for " + finalFolder + ", request to use " + pkg + " is ignored.");
 215             return false;
 216         }
 217     }
 218 
 219     static class Entity {
 220         final String pkg;
 221         final String entity;
 222 
 223         Entity(String pkg, String entity) {
 224             this.pkg = pkg;
 225             this.entity = entity;
 226         }
 227     }
 228 
 229     /**
 230      * Determine package and interface name given a path. If the path is
 231      * a folder, then only package name is determined. The package name is
 232      * determined with the longest path matching the setup. If the path is not
 233      * setup for any package, the default package name is returned.
 234      *
 235      * @param origin The source path
 236      * @return The Entity
 237      * @see Context::usePackageForFolder(Path, String)
 238      */
 239     Entity whatis(Path origin) {
 240         // normalize to absolute path
 241         origin = origin.toAbsolutePath();
 242         String filename = null;
 243         if (!Files.isDirectory(origin)) {
 244             // ensure it's a folder name
 245             filename = origin.getFileName().toString();
 246             origin = origin.getParent();
 247         }
 248         Path path = origin;
 249 
 250         // search the map for a hit with longest path
 251         while (path != null && !pkgMap.containsKey(path)) {
 252             path = path.getParent();
 253         }
 254 
 255         int start;
 256         String pkg;
 257         if (path != null) {
 258             start = path.getNameCount();
 259             pkg = pkgMap.get(path);
 260         } else {
 261             pkg = pkgMap.get(null);
 262             if (pkg == null) {
 263                 start = 0;
 264                 pkg = defaultPkg;
 265             } else {
 266                 start = origin.getNameCount();
 267             }
 268         }
 269 
 270         if (filename == null) {
 271             // a folder, only pkg name matters
 272             return new Entity(pkg, null);
 273         }
 274 
 275         StringBuilder sb = new StringBuilder();
 276         while (start < origin.getNameCount()) {
 277             sb.append(Utils.toJavaIdentifier(origin.getName(start++).toString()));
 278             sb.append("_");
 279         }
 280 
 281         int ext = filename.lastIndexOf('.');
 282         if (ext != -1) {
 283             sb.append(filename.substring(0, ext));
 284         } else {
 285             sb.append(filename);
 286         }
 287         return new Entity(pkg, Utils.toClassName(sb.toString()));
 288     }
 289 
 290     HeaderFile getHeaderFile(Path header, HeaderFile main) {
 291         if (!Files.isRegularFile(header)) {
 292             logger.warning(() -> "Not a regular file: " + header.toString());
 293             throw new IllegalArgumentException(header.toString());
 294         }
 295 
 296         final Context.Entity e = whatis(header);
 297         return new HeaderFile(this, header, e.pkg, e.entity, main);
 298     }
 299 
 300     void processTree(Tree tree, HeaderFile main, Function<HeaderFile, AsmCodeFactory> fn) {
 301         SourceLocation loc = tree.location();
 302 
 303         HeaderFile header;
 304         boolean isBuiltIn = false;
 305 
 306         if (tree.isFromMain()) {
 307             header = main;
 308         } else {
 309             SourceLocation.Location src = loc.getFileLocation();
 310             if (src == null) {
 311                 logger.info(() -> "Tree " + tree.name() + "@" + tree.USR() + " has no FileLocation");
 312                 return;
 313             }
 314 
 315             Path p = src.path();
 316             if (p == null) {
 317                 logger.fine(() -> "Found built-in type: " + tree.name());
 318                 header = main;
 319                 isBuiltIn = true;
 320             } else {
 321                 p = p.normalize().toAbsolutePath();
 322                 header = headerMap.get(p);
 323                 if (header == null) {
 324                     final HeaderFile hf = header = getHeaderFile(p, main);
 325                     logger.config(() -> "First encounter of header file " + hf.path + ", assigned to package " + hf.pkgName);
 326                     // Only generate code for header files sepcified or in the same package
 327                     // System headers are excluded, they need to be explicitly specified in jextract cmdline
 328                     if (sources.contains(p) ||
 329                         (!loc.isInSystemHeader()) && (header.pkgName.equals(main.pkgName))) {
 330                         logger.config("Code gen for header " + p + " enabled in package " + header.pkgName);
 331                         header.useCodeFactory(fn.apply(header));
 332                     }
 333                     headerMap.put(p, header);
 334                 }
 335             }
 336         }
 337 
 338         header.processTree(tree, main, isBuiltIn);
 339     }
 340 
 341     public void parse() {
 342         parse(header -> new AsmCodeFactory(this, header));
 343     }
 344 
 345     private boolean symbolFilter(Tree tree) {
 346          String name = tree.name();
 347          if (isSymbolExcluded(name)) {
 348              return false;
 349          }
 350 
 351          // check for function symbols in libraries & warn missing symbols
 352          if (tree instanceof FunctionTree && !isSymbolFound(name)) {
 353              err.println(Main.format("warn.symbol.not.found", name));
 354          }
 355 
 356          return true;
 357     }
 358 
 359     public void parse(Function<HeaderFile, AsmCodeFactory> fn) {
 360         initSymChecker();
 361         initSymFilter();
 362 
 363         List<HeaderTree> headers = parser.parse(sources, clangArgs);
 364         processHeaders(headers, fn);
 365     }
 366 
 367     private void processHeaders(List<HeaderTree> headers, Function<HeaderFile, AsmCodeFactory> fn) {
 368         headers.stream().
 369                 map(new TreeFilter(this::symbolFilter)).
 370                 map(new TypedefHandler()).
 371                 map(new EmptyNameHandler()).
 372                 forEach(header -> {
 373             HeaderFile hf = headerMap.computeIfAbsent(header.path(), p -> getHeaderFile(p, null));
 374             hf.useLibraries(libraryNames, libraryPaths);
 375             hf.useCodeFactory(fn.apply(hf));
 376             logger.info(() -> "Processing header file " + header.path());
 377 
 378             header.declarations().stream()
 379                     .peek(decl -> logger.finest(
 380                         () -> "Cursor: " + decl.name() + "@" + decl.USR() + "?" + decl.isDeclaration()))
 381                     .forEach(decl -> processTree(decl, hf, fn));
 382         });
 383     }
 384 
 385     private Map<String, List<AsmCodeFactory>> getPkgCfMap() {
 386         final Map<String, List<AsmCodeFactory>> mapPkgCf = new HashMap<>();
 387         // Build the pkg to CodeFactory map
 388         headerMap.values().forEach(header -> {
 389             AsmCodeFactory cf = header.getCodeFactory();
 390             String pkg = header.pkgName;
 391             logger.config(() -> "File " + header + " is in package: " + pkg);
 392             if (cf == null) {
 393                 logger.config(() -> "File " + header + " code generation is not activated!");
 394                 return;
 395             }
 396             List<AsmCodeFactory> l = mapPkgCf.computeIfAbsent(pkg, k -> new ArrayList<>());
 397             l.add(cf);
 398             logger.config(() -> "Add cf " + cf + " to pkg " + pkg + ", size is now " + l.size());
 399         });
 400         return Collections.unmodifiableMap(mapPkgCf);
 401     }
 402 
 403     public Map<String, byte[]> collectClasses(String... pkgs) {
 404         final Map<String, byte[]> rv = new HashMap<>();
 405         final Map<String, List<AsmCodeFactory>> mapPkgCf = getPkgCfMap();
 406         for (String pkg_name : pkgs) {
 407             mapPkgCf.getOrDefault(pkg_name, Collections.emptyList())
 408                     .forEach(cf -> rv.putAll(cf.collect()));
 409         }
 410         return Collections.unmodifiableMap(rv);
 411     }
 412 
 413     private static final String JEXTRACT_MANIFEST = "META-INFO" + File.separatorChar + "jextract.properties";
 414 
 415     @SuppressWarnings("deprecation")
 416     private byte[] getJextractProperties(String[] args) {
 417         Properties props = new Properties();
 418         props.setProperty("os.name", System.getProperty("os.name"));
 419         props.setProperty("os.version", System.getProperty("os.version"));
 420         props.setProperty("os.arch", System.getProperty("os.arch"));
 421         props.setProperty("jextract.args", Arrays.toString(args));
 422         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 423         props.save(baos, "jextract meta data");
 424         return baos.toByteArray();
 425     }
 426 
 427     void collectClassFiles(Path destDir, String[] args, String... pkgs) throws IOException {
 428         try {
 429             collectClasses(pkgs).entrySet().stream().forEach(e -> {
 430                 try {
 431                     String path = e.getKey().replace('.', File.separatorChar) + ".class";
 432                     logger.fine(() -> "Writing " + path);
 433                     Path fullPath = destDir.resolve(path).normalize();
 434                     Files.createDirectories(fullPath.getParent());
 435                     try (OutputStream fos = Files.newOutputStream(fullPath)) {
 436                         fos.write(e.getValue());
 437                         fos.flush();
 438                     }
 439                 } catch (IOException ioe) {
 440                     throw new UncheckedIOException(ioe);
 441                 }
 442             });
 443 
 444             Path propsPath = destDir.resolve(JEXTRACT_MANIFEST).normalize();
 445             Files.createDirectories(propsPath.getParent());
 446             try (OutputStream fos = Files.newOutputStream(propsPath)) {
 447                 fos.write(getJextractProperties(args));
 448                 fos.flush();
 449             }
 450         } catch (UncheckedIOException uioe) {
 451             throw uioe.getCause();
 452         }
 453     }
 454 
 455     private void writeJar(AsmCodeFactory cf, JarOutputStream jar) {
 456         cf.collect().entrySet().stream().forEach(e -> {
 457             try {
 458                 String path = e.getKey().replace('.', File.separatorChar) + ".class";
 459                 logger.fine(() -> "Add " + path);
 460                 jar.putNextEntry(new ZipEntry(path));
 461                 jar.write(e.getValue());
 462                 jar.closeEntry();
 463             } catch (IOException ioe) {
 464                 throw new UncheckedIOException(ioe);
 465             }
 466         });
 467     }
 468 
 469     public void collectJarFile(final JarOutputStream jos, String[] args, String... pkgs) {
 470         final Map<String, List<AsmCodeFactory>> mapPkgCf = getPkgCfMap();
 471 
 472         for (String pkg_name : pkgs) {
 473             // convert '.' to '/' to use as a path
 474             String entryName = Utils.toInternalName(pkg_name, "");
 475             // package folder
 476             if (!entryName.isEmpty()) {
 477                 try {
 478                     jos.putNextEntry(new ZipEntry(entryName));
 479                 } catch (IOException ex) {
 480                     throw new UncheckedIOException(ex);
 481                 }
 482             }
 483             logger.fine(() -> "Produce for package " + pkg_name);
 484             mapPkgCf.getOrDefault(pkg_name, Collections.emptyList())
 485                     .forEach(cf -> writeJar(cf, jos));
 486         }
 487 
 488         try {
 489             jos.putNextEntry(new ZipEntry(JEXTRACT_MANIFEST));
 490             jos.write(getJextractProperties(args));
 491             jos.closeEntry();
 492         } catch (IOException ioe) {
 493             throw new UncheckedIOException(ioe);
 494         }
 495     }
 496 
 497     void collectJarFile(final Path jar, String[] args, String... pkgs) throws IOException {
 498         logger.info(() -> "Collecting jar file " + jar);
 499         try (OutputStream os = Files.newOutputStream(jar, CREATE, TRUNCATE_EXISTING, WRITE);
 500                 JarOutputStream jo = new JarOutputStream(os)) {
 501             collectJarFile(jo, args, pkgs);
 502         } catch (UncheckedIOException uioe) {
 503             throw uioe.getCause();
 504         }
 505     }
 506 
 507     /**
 508      * Perform a local lookup, any undefined type will cause a JType
 509      * be defined within origin scope.
 510      *
 511      * @param type   The libclang type
 512      * @param origin The path of the file where type is encountered
 513      * @return The JType
 514      */
 515     JType getJType(final Type type, Path origin) {
 516         Path p = origin.normalize().toAbsolutePath();
 517 
 518         HeaderFile hf = headerMap.get(p);
 519         // We should not encounter a type if the header file reference to it is not yet processed
 520         assert(null != hf);
 521         if (hf == null) {
 522             throw new IllegalArgumentException("Failed to lookup header for " + p + " (origin: " + origin + ")");
 523         }
 524 
 525         return hf.localLookup(type);
 526     }
 527 
 528     /**
 529      * Perform a global lookup
 530      *
 531      * @param c The cursor define or declare the type.
 532      * @return
 533      */
 534     JType getJType(final Cursor c) {
 535         if (c.isInvalid()) {
 536             throw new IllegalArgumentException();
 537         }
 538         SourceLocation loc = c.getSourceLocation();
 539         if (null == loc) {
 540             return null;
 541         }
 542         Path p = loc.getFileLocation().path();
 543         if (null == p) {
 544             return null;
 545         }
 546         return getJType(c.type(), p);
 547     }
 548 }