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 ---