1 /*
  2  * Copyright (c) 2020, 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.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package jdk.incubator.jextract;
 27 
 28 import jdk.internal.jextract.impl.ClangException;
 29 import jdk.internal.jextract.impl.IncludeHelper;
 30 import jdk.internal.jextract.impl.OutputFactory;
 31 import jdk.internal.jextract.impl.Parser;
 32 import jdk.internal.jextract.impl.Options;
 33 import jdk.internal.jextract.impl.Writer;
 34 import jdk.internal.joptsimple.OptionException;
 35 import jdk.internal.joptsimple.OptionParser;
 36 import jdk.internal.joptsimple.OptionSet;
 37 
 38 import javax.tools.JavaFileObject;
 39 import java.io.File;
 40 import java.io.IOException;
 41 import java.io.PrintWriter;
 42 import java.io.UncheckedIOException;
 43 import java.nio.file.Files;
 44 import java.nio.file.Path;
 45 import java.nio.file.Paths;
 46 import java.text.MessageFormat;
 47 import java.util.List;
 48 import java.util.Locale;
 49 import java.util.ResourceBundle;
 50 import java.util.spi.ToolProvider;
 51 import java.util.stream.Collectors;
 52 import java.util.stream.Stream;
 53 
 54 /**
 55  * Simple extraction tool which generates a minimal Java API. Such an API consists mainly of static methods,
 56  * where for each native function a static method is added which calls the underlying native method handles.
 57  * Similarly, for struct fields and global variables, static accessors (getter and setter) are generated
 58  * on top of the underlying memory access var handles. For each struct, a static layout field is generated.
 59  */
 60 public final class JextractTool {
 61     private static final String MESSAGES_RESOURCE = "jdk.internal.jextract.impl.resources.Messages";
 62 
 63     private static final ResourceBundle MESSAGES_BUNDLE;
 64     static {
 65         MESSAGES_BUNDLE = ResourceBundle.getBundle(MESSAGES_RESOURCE, Locale.getDefault());
 66     }
 67 
 68     public static final boolean DEBUG = Boolean.getBoolean("jextract.debug");
 69 
 70     // error codes
 71     private static final int SUCCESS       = 0;
 72     private static final int OPTION_ERROR  = 1;
 73     private static final int INPUT_ERROR   = 2;
 74     private static final int CLANG_ERROR   = 3;
 75     private static final int RUNTIME_ERROR = 4;
 76     private static final int OUTPUT_ERROR  = 5;
 77 
 78     private final PrintWriter out;
 79     private final PrintWriter err;
 80 
 81     private static String format(String msgId, Object... args) {
 82         return new MessageFormat(MESSAGES_BUNDLE.getString(msgId)).format(args);
 83     }
 84 
 85     private JextractTool(PrintWriter out, PrintWriter err) {
 86         this.out = out;
 87         this.err = err;
 88     }
 89 
 90     private static Path generateTmpSource(List<Path> headers) {
 91         assert headers.size() > 1;
 92         try {
 93             Path tmpFile = Files.createTempFile("jextract", ".h");
 94             tmpFile.toFile().deleteOnExit();
 95             Files.write(tmpFile, headers.stream().
 96                     map(src -> "#include \"" + src + "\"").
 97                     collect(Collectors.toList()));
 98             return tmpFile;
 99         } catch (IOException ioExp) {
100             throw new UncheckedIOException(ioExp);
101         }
102     }
103 
104     /**
105      * Parse input files into a toplevel declaration with given options.
106      * @param parserOptions options to be passed to the parser.
107      * @return a toplevel declaration.
108      */
109     public static Declaration.Scoped parse(List<Path> headers, String... parserOptions) {
110         Path source = headers.size() > 1? generateTmpSource(headers) : headers.iterator().next();
111         return new Parser().parse(source, Stream.of(parserOptions).collect(Collectors.toList()));
112     }
113 
114     public static List<JavaFileObject> generate(Declaration.Scoped decl, String headerName,
115                                                 String targetPkg, List<String> libNames) {
116         return List.of(OutputFactory.generateWrapped(decl, headerName, targetPkg, new IncludeHelper(), libNames));
117     }
118 
119     private static List<JavaFileObject> generateInternal(Declaration.Scoped decl, String headerName,
120                                                 String targetPkg, IncludeHelper includeHelper, List<String> libNames) {
121         return List.of(OutputFactory.generateWrapped(decl, headerName, targetPkg, includeHelper, libNames));
122     }
123 
124     /**
125      * Write resulting {@link JavaFileObject} instances into specified destination path.
126      * @param dest the destination path.
127      * @param compileSources whether to compile .java sources or not
128      * @param files the {@link JavaFileObject} instances to be written.
129      */
130     public static void write(Path dest, boolean compileSources, List<JavaFileObject> files) throws UncheckedIOException {
131         try {
132             new Writer(dest, files).writeAll(compileSources);
133         } catch (IOException ex) {
134             throw new UncheckedIOException(ex);
135         }
136     }
137 
138     private int printHelp(OptionParser parser, int exitCode) {
139         try {
140             parser.printHelpOn(err);
141         } catch (IOException ignored) {}
142         return exitCode;
143     }
144 
145 
146     private void printOptionError(Throwable throwable) {
147         printOptionError(throwable.getMessage());
148         if (DEBUG) {
149             throwable.printStackTrace(err);
150         }
151     }
152 
153     private void printOptionError(String message) {
154         err.println("OPTION ERROR: " + message);
155         err.println("Usage: jextract <options> [--] <header file>");
156         err.println("Use --help for a list of possible options");
157     }
158 
159     /**
160      * Main entry point to run the JextractTool
161      *
162      * @param args command line options passed
163      */
164     public static void main(String[] args) {
165         if (args.length == 0) {
166             System.err.println("Expected a header file");
167             return;
168         }
169 
170         JextractTool m = new JextractTool(new PrintWriter(System.out, true), new PrintWriter(System.err, true));
171         System.exit(m.run(args));
172     }
173 
174     private int run(String[] args) {
175         OptionParser parser = new OptionParser(false);
176         parser.accepts("C", format("help.C")).withRequiredArg();
177         parser.accepts("I", format("help.I")).withRequiredArg();
178         parser.accepts("d", format("help.d")).withRequiredArg();
179         parser.accepts("dump-includes", format("help.dump-includes")).withRequiredArg();
180         for (IncludeHelper.IncludeKind includeKind : IncludeHelper.IncludeKind.values()) {
181             parser.accepts(includeKind.optionName(), format("help." + includeKind.optionName())).withRequiredArg();
182         }
183         parser.accepts("l", format("help.l")).withRequiredArg();
184         parser.accepts("source", format("help.source"));
185         parser.acceptsAll(List.of("t", "target-package"), format("help.t")).withRequiredArg();
186         parser.acceptsAll(List.of("?", "h", "help"), format("help.h")).forHelp();
187         parser.accepts("header-class-name", format("help.header-class-name")).withRequiredArg();
188         parser.nonOptions(format("help.non.option"));
189 
190         OptionSet optionSet;
191         try {
192             optionSet = parser.parse(args);
193         } catch (OptionException oe) {
194             printOptionError(oe);
195             return OPTION_ERROR;
196         }
197 
198         if (optionSet.has("h")) {
199             return printHelp(parser, SUCCESS);
200         }
201 
202         if (optionSet.nonOptionArguments().size() != 1) {
203             printOptionError("Expected 1 header file, not " + optionSet.nonOptionArguments().size());
204             return OPTION_ERROR;
205         }
206 
207         Options.Builder builder = Options.builder();
208         if (optionSet.has("I")) {
209             optionSet.valuesOf("I").forEach(p -> builder.addClangArg("-I" + p));
210         }
211 
212         Path builtinInc = Paths.get(System.getProperty("java.home"), "conf", "jextract");
213         builder.addClangArg("-I" + builtinInc);
214 
215         if (optionSet.has("C")) {
216             optionSet.valuesOf("C").forEach(p -> builder.addClangArg((String) p));
217         }
218 
219         if (optionSet.has("filter")) {
220             optionSet.valuesOf("filter").forEach(p -> builder.addFilter((String) p));
221         }
222 
223         for (IncludeHelper.IncludeKind includeKind : IncludeHelper.IncludeKind.values()) {
224             if (optionSet.has(includeKind.optionName())) {
225                 optionSet.valuesOf(includeKind.optionName()).forEach(p -> builder.addIncludeSymbol(includeKind, (String)p));
226             }
227         }
228 
229         if (optionSet.has("dump-includes")) {
230             builder.setDumpIncludeFile(optionSet.valueOf("dump-includes").toString());
231         }
232 
233         if (optionSet.has("d")) {
234             builder.setOutputDir(optionSet.valueOf("d").toString());
235         }
236 
237         if (optionSet.has("source")) {
238             builder.setGenerateSource();
239         }
240         boolean librariesSpecified = optionSet.has("l");
241         if (librariesSpecified) {
242             for (Object arg : optionSet.valuesOf("l")) {
243                 String lib = (String)arg;
244                 if (lib.indexOf(File.separatorChar) == -1) {
245                     builder.addLibraryName(lib);
246                 } else {
247                     Path libPath = Paths.get(lib);
248                     if (libPath.isAbsolute() && Files.isRegularFile(libPath)) {
249                         builder.addLibraryName(lib);
250                     } else {
251                         err.println(format("l.option.value.invalid", lib));
252                         return OPTION_ERROR;
253                     }
254                 }
255             }
256         }
257 
258         String targetPackage = optionSet.has("t") ? (String) optionSet.valueOf("t") : "";
259         builder.setTargetPackage(targetPackage);
260 
261         Options options = builder.build();
262 
263         Path header = Paths.get(optionSet.nonOptionArguments().get(0).toString());
264         if (!Files.isReadable(header)) {
265             err.println(format("cannot.read.header.file", header));
266             return INPUT_ERROR;
267         }
268         if (!(Files.isRegularFile(header))) {
269             err.println(format("not.a.file", header));
270             return INPUT_ERROR;
271         }
272 
273         List<JavaFileObject> files = null;
274         try {
275             Declaration.Scoped toplevel = parse(List.of(header), options.clangArgs.toArray(new String[0]));
276 
277             if (JextractTool.DEBUG) {
278                 System.out.println(toplevel);
279             }
280 
281             String headerName = optionSet.has("header-class-name") ?
282                 (String) optionSet.valueOf("header-class-name") :
283                 header.getFileName().toString();
284 
285             files = generateInternal(
286                 toplevel, headerName,
287                 options.targetPackage, options.includeHelper, options.libraryNames);
288         } catch (ClangException ce) {
289             err.println(ce.getMessage());
290             if (JextractTool.DEBUG) {
291                 ce.printStackTrace(err);
292             }
293             return CLANG_ERROR;
294         } catch (RuntimeException re) {
295             err.println(re.getMessage());
296             if (JextractTool.DEBUG) {
297                 re.printStackTrace(err);
298             }
299             return RUNTIME_ERROR;
300         }
301 
302         try {
303             if (options.includeHelper.dumpIncludesFile != null) {
304                 options.includeHelper.dumpIncludes();
305             } else {
306                 Path output = Path.of(options.outputDir);
307                 write(output, !options.source, files);
308             }
309         } catch (UncheckedIOException uioe) {
310             err.println(uioe.getMessage());
311             if (JextractTool.DEBUG) {
312                 uioe.printStackTrace(err);
313             }
314             return OUTPUT_ERROR;
315         } catch (RuntimeException re) {
316             err.println(re.getMessage());
317             if (JextractTool.DEBUG) {
318                 re.printStackTrace(err);
319             }
320             return RUNTIME_ERROR;
321         }
322 
323         return SUCCESS;
324     }
325 
326     /**
327      * ToolProvider implementation for jextract tool.
328      */
329     public static class JextractToolProvider implements ToolProvider {
330         public JextractToolProvider() {}
331 
332         @Override
333         public String name() {
334             return "jextract";
335         }
336 
337         @Override
338         public int run(PrintWriter out, PrintWriter err, String... args) {
339             JextractTool instance = new JextractTool(out, err);
340             return instance.run(args);
341         }
342     }
343 }