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 }