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