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