From e7b0c093f4a8f0354021b0bd1a70351ab53fa0b2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 6 Dec 2019 04:44:07 +0800 Subject: update gjavah version --- .../ch/jodersky/sbt/jni/javah/ClassMetaInfo.java | 46 +++ .../java/ch/jodersky/sbt/jni/javah/ClassName.java | 86 +++++ .../java/ch/jodersky/sbt/jni/javah/ClassPath.java | 43 +++ .../java/ch/jodersky/sbt/jni/javah/Constant.java | 81 +++++ .../ch/jodersky/sbt/jni/javah/HeaderGenerator.java | 397 --------------------- .../ch/jodersky/sbt/jni/javah/JNIGenerator.java | 236 ++++++++++++ .../java/ch/jodersky/sbt/jni/javah/JavahTask.java | 81 +++++ .../java/ch/jodersky/sbt/jni/javah/ModulePath.java | 50 +++ .../ch/jodersky/sbt/jni/javah/NativeMethod.java | 90 +++++ .../jodersky/sbt/jni/javah/RuntimeSearchPath.java | 44 +++ .../java/ch/jodersky/sbt/jni/javah/SearchPath.java | 90 +++++ .../main/java/ch/jodersky/sbt/jni/javah/Utils.java | 133 +++++++ .../ch/jodersky/sbt/jni/plugins/JniJavah.scala | 11 +- 13 files changed, 986 insertions(+), 402 deletions(-) create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassMetaInfo.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassName.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassPath.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/Constant.java delete mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/JNIGenerator.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/JavahTask.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/ModulePath.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/NativeMethod.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/RuntimeSearchPath.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/SearchPath.java create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/Utils.java diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassMetaInfo.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassMetaInfo.java new file mode 100644 index 0000000..c1e5b6b --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassMetaInfo.java @@ -0,0 +1,46 @@ +package ch.jodersky.sbt.jni.javah; + +import org.objectweb.asm.*; + +import java.util.*; + +class ClassMetaInfo extends ClassVisitor { + final List constants = new LinkedList<>(); + final List methods = new LinkedList<>(); + final Map counts = new HashMap<>(); + + ClassName superClassName; + ClassName name; + + public ClassMetaInfo() { + super(Opcodes.ASM6); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.superClassName = superName == null ? null : ClassName.of(superName.replace('/', '.')); + this.name = ClassName.of(name.replace('/', '.')); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + counts.put(name, counts.getOrDefault(name, 0) + 1); + if ((access & Opcodes.ACC_NATIVE) != 0) { + this.methods.add(NativeMethod.of(access, name, descriptor)); + } + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (value != null && !(value instanceof String)) { + constants.add(Constant.of(name, value)); + } + return null; + } + + boolean isOverloadMethod(NativeMethod method) { + Objects.requireNonNull(method); + return counts.getOrDefault(method.name(), 1) > 1; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassName.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassName.java new file mode 100644 index 0000000..8cb8f28 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassName.java @@ -0,0 +1,86 @@ +package ch.jodersky.sbt.jni.javah; + +import java.util.Objects; + +public final class ClassName { + private final String moduleName; + private final String className; + private final String simpleName; + private final String mangledName; + + public static ClassName of(String moduleName, String className) { + Objects.requireNonNull(className, "Class name is null"); + + if (moduleName != null && !Utils.FULL_NAME_PATTERN.matcher(moduleName).matches()) { + throw new IllegalArgumentException("Illegal module name: " + moduleName); + } + if (!Utils.FULL_NAME_PATTERN.matcher(className).matches()) { + throw new IllegalArgumentException("Illegal class name: " + moduleName); + } + + return new ClassName(moduleName, className); + } + + public static ClassName of(String fullName) { + Objects.requireNonNull(fullName, "class name is null"); + int idx = fullName.indexOf('/'); + if (idx == -1) { + return ClassName.of(null, fullName); + } + + return ClassName.of(fullName.substring(0, idx), fullName.substring(idx + 1)); + } + + private ClassName(String moduleName, String className) { + this.moduleName = moduleName; + this.className = className; + this.simpleName = className.substring(className.lastIndexOf('.') + 1); + this.mangledName = Utils.mangleName(className); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ClassName)) return false; + ClassName className1 = (ClassName) o; + return Objects.equals(moduleName, className1.moduleName) && className.equals(className1.className); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, className); + } + + @Override + public String toString() { + if (moduleName == null) { + return className; + } + return moduleName + '/' + className; + } + + + // + // Getters and Setters + // + + public final String moduleName() { + return moduleName; + } + + public final String className() { + return className; + } + + public final String simpleName() { + return simpleName; + } + + public final String mangledName() { + return mangledName; + } + + public final String relativePath() { + return className.replace('.', '/') + ".class"; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassPath.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassPath.java new file mode 100644 index 0000000..47de2ee --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ClassPath.java @@ -0,0 +1,43 @@ +package ch.jodersky.sbt.jni.javah; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ClassPath implements SearchPath { + private final Path path; + private final List roots; + + public ClassPath(Path path) { + Objects.requireNonNull(path); + this.path = path.toAbsolutePath(); + + Path root = Utils.classPathRoot(path); + roots = root == null ? Collections.emptyList() : multiReleaseRoots(root); + } + + @Override + public Path search(ClassName name) { + Objects.requireNonNull(name); + return searchFromRoots(roots, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClassPath classPath = (ClassPath) o; + return Objects.equals(path, classPath.path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public String toString() { + return "ClassPath[" + path + "]"; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Constant.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Constant.java new file mode 100644 index 0000000..3aca9ac --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Constant.java @@ -0,0 +1,81 @@ +package ch.jodersky.sbt.jni.javah; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public final class Constant { + private static final List> TYPES = Arrays.asList( + Byte.class, Short.class, Integer.class, Long.class, Character.class, Float.class, Double.class + ); + + private final String name; + private final Object value; + private final String mangledName; + + public static Constant of(String name, Object value) { + Objects.requireNonNull(name); + Objects.requireNonNull(value); + + if (!TYPES.contains(value.getClass())) { + throw new IllegalArgumentException(); + } + if (!Utils.SIMPLE_NAME_PATTERN.matcher(name).matches()) { + throw new IllegalArgumentException(String.format("\"%s\" is not a qualified constant name", name)); + } + + return new Constant(name, value); + } + + private Constant(String name, Object value) { + this.name = name; + this.value = value; + this.mangledName = Utils.mangleName(name); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Constant)) return false; + Constant constant = (Constant) o; + return name.equals(constant.name) && value.equals(constant.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + @Override + public String toString() { + return String.format("Constant[name=%s, value=%s]", name, value); + } + + public String name() { + return name; + } + + public Object value() { + return value; + } + + public String mangledName() { + return mangledName; + } + + public String valueToString() { + if (value instanceof Double) { + return value.toString(); + } + if (value instanceof Float) { + return value + "f"; + } + if (value instanceof Long) { + return value + "i64"; + } + if (value instanceof Character) { + return ((int) (char) value) + "L"; + } + return value + "L"; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java deleted file mode 100644 index a26aa20..0000000 --- a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java +++ /dev/null @@ -1,397 +0,0 @@ -package ch.jodersky.sbt.jni.javah; - -/* - * Code is copied from (https://github.com/Glavo/gjavah) - * MIT License - */ - -import org.objectweb.asm.*; - -import java.io.*; -import java.nio.file.*; -import java.util.*; - -public final class HeaderGenerator { - private static final Path[] EMPTY_PATH_ARRAY = new Path[0]; - private static final List THROWABLE_NAME_LIST = Arrays.asList("Ljava/lang/Throwable;", "Ljava/lang/Error;", "Ljava/lang/Exception"); - private static final HeaderGenerator generator = new HeaderGenerator(); - - private static String escape(String source, Boolean escapeDots) { - StringBuilder builder = new StringBuilder(); - char ch; - for (int i = 0; i < source.length(); i++) { - switch (ch = source.charAt(i)) { - case '_': - builder.append("_1"); - break; - case ';': - builder.append("_2"); - break; - case '[': - builder.append("_3"); - break; - case '/': - if(escapeDots) - builder.append('_'); - else - builder.append('.'); - break; - default: - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { - builder.append(ch); - } else { - builder.append("_0").append(String.format("%04x", (int) ch)); - } - } - } - return builder.toString(); - } - - public static void generateFunctionDeclarations(ClassReader reader, PrintWriter output) { - getGenerator().classGenerateFunctionDeclarations(reader, output); - } - - public static void generateHeader(ClassReader reader, PrintWriter output) { - getGenerator().classGenerateHeader(reader, output); - } - - public static void generateHeader(byte[] classFile, PrintWriter output) { - getGenerator().classGenerateHeader(classFile, output); - } - - public static void generateHeader(byte[] classFileBuffer, int classFileOffset, int classFileLength, PrintWriter output) { - getGenerator().classGenerateHeader(classFileBuffer, classFileOffset, classFileLength, output); - } - - public static void generateHeader(String className, PrintWriter output) throws IOException { - getGenerator().classGenerateHeader(className, output); - } - - public static void generateHeader(InputStream input, PrintWriter output) throws IOException { - getGenerator().classGenerateHeader(input, output); - } - - public static HeaderGenerator getGenerator() { - return generator; - } - - private Path[] classPaths; - private boolean useRuntimeClassPath; - - public HeaderGenerator() { - this(EMPTY_PATH_ARRAY, true); - } - - public HeaderGenerator(boolean useRuntimeClassPath) { - this(EMPTY_PATH_ARRAY, useRuntimeClassPath); - } - - public HeaderGenerator(Path[] classPaths) { - this(classPaths, true); - } - - public HeaderGenerator(Path[] classPaths, boolean useRuntimeClassPath) { - Objects.requireNonNull(classPaths); - this.classPaths = classPaths; - this.useRuntimeClassPath = useRuntimeClassPath; - } - - private boolean isThrowable(Type type) { - String desc = type.getDescriptor(); - if (!desc.startsWith("L")) { - return false; - } - if (classPaths.length == 0 && !useRuntimeClassPath) { - return THROWABLE_NAME_LIST.contains(type.getDescriptor()); - } - String className = type.getInternalName(); - while (true) { - if (className == null) { - return false; - } - if (className.equals("java/lang/Throwable")) { - return true; - } - try { - ClassReader reader = findClass(className); - if (reader == null) { - return false; - } - className = reader.getSuperName(); - } catch (Exception ignored) { - return false; - } - } - } - - private String typeToNative(Type tpe) { - if (tpe == Type.BOOLEAN_TYPE) { - return "jboolean"; - } else if (tpe == Type.BYTE_TYPE) { - return "jbyte"; - } else if (tpe == Type.CHAR_TYPE) { - return "jchar"; - } else if (tpe == Type.SHORT_TYPE) { - return "jshort"; - } else if (tpe == Type.INT_TYPE) { - return "jint"; - } else if (tpe == Type.LONG_TYPE) { - return "jlong"; - } else if (tpe == Type.FLOAT_TYPE) { - return "jfloat"; - } else if (tpe == Type.DOUBLE_TYPE) { - return "jdouble"; - } else if (tpe == Type.VOID_TYPE) { - return "void"; - } else { - String desc = tpe.getDescriptor(); - if (desc.startsWith("[")) { - Type elemTpe = tpe.getElementType(); - String descriptor = elemTpe.getDescriptor(); - if (descriptor.startsWith("[") || descriptor.startsWith("L")) { - return "jobjectArray"; - } - return typeToNative(elemTpe) + "Array"; - } - if (desc.equals("Ljava/lang/String;")) { - return "jstring"; - } - if (desc.equals("Ljava/lang/Class;")) { - return "jclass"; - } - if (isThrowable(tpe)) { - return "jthrowable"; - } - return "jobject"; - } - } - - private ClassReader findClass(String name) { - loop: - for (Path path : classPaths) { - path = path.toAbsolutePath(); - if (!Files.exists(path)) { - continue; - } - String[] ps = name.split("/|\\."); - if (ps.length == 0) { - continue; - } - ps[ps.length - 1] += ".class"; - for (String p : ps) { - path = path.resolve(p); - if (!Files.exists(path)) { - continue loop; - } - } - try { - return new ClassReader(Files.newInputStream(path)); - } catch (IOException ignored) { - } - } - try { - return new ClassReader(name.replace('/', '.')); - } catch (IOException e) { - return null; - } - } - - private void classGenerateFunctionDeclarations(Generator generator, PrintWriter output) { - String className = escape(generator.getClassName(), false); - String classNameNoDots = escape(generator.getClassName(), true); - - for (Map.Entry> entry : generator.getMethods().entrySet()) { - boolean overload = entry.getValue().size() > 1; - for (MethodDesc desc : entry.getValue()) { - String methodName = escape(entry.getKey(), false); - output.println("/*" + "\n" + - " * Class: " + className + "\n" + - " * Method: " + entry.getKey() + "\n" + - " * Signature: " + desc.descriptor + "\n" + - " */" - ); - - Type[] argTypes = Type.getArgumentTypes(desc.descriptor); - Type retType = Type.getReturnType(desc.descriptor); - - output.print( - "JNIEXPORT " + typeToNative(retType) + " JNICALL Java_" + classNameNoDots + "_" + methodName - ); - - if (overload) { - output.print("__"); - for (Type tpe : argTypes) { - output.print(escape(tpe.toString(), true)); - } - } - output.println(); - - output.print(" (JNIEnv *, "); - if (desc.isStatic) { - output.print("jclass"); - } else { - output.print("jobject"); - } - for (Type tpe : argTypes) { - output.print(", " + typeToNative(tpe)); - } - output.println(");\n"); - } - - } - } - - public void classGenerateFunctionDeclarations(ClassReader reader, PrintWriter output) { - Generator generator = new Generator(); - reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - classGenerateFunctionDeclarations(generator, output); - } - - private void classGenerateHeaderWithoutInclude(Generator generator, PrintWriter output) { - String className = escape(generator.getClassName(), true); - - output.println("/* Header for class " + className + " */"); - - String includeHeader = "_Include_" + className; - output.println("#ifndef " + includeHeader); - output.println("#define " + includeHeader); - - output.println("#ifdef __cplusplus\n" + - "extern \"C\" {\n" + - "#endif"); - - classGenerateFunctionDeclarations(generator, output); - - output.println("#ifdef __cplusplus\n" + - "}\n" + - "#endif\n" + - "#endif\n"); - } - - private void classGenerateHeaderWithoutInclude(ClassReader reader, PrintWriter output) { - Generator generator = new Generator(); - reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - classGenerateHeaderWithoutInclude(generator, output); - } - - private void classGenerateHeader(Generator generator, PrintWriter output) { - output.println("/* DO NOT EDIT THIS FILE - it is machine generated */"); - output.println("#include "); - - classGenerateHeaderWithoutInclude(generator, output); - } - - public void classGenerateHeader(ClassReader reader, PrintWriter output) { - Generator generator = new Generator(); - reader.accept(generator, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - classGenerateHeader(generator, output); - } - - public void classGenerateHeader(byte[] classFile, PrintWriter output) { - classGenerateHeader(new ClassReader(classFile), output); - } - - public void classGenerateHeader(byte[] classFileBuffer, int classFileOffset, int classFileLength, PrintWriter output) { - classGenerateHeader(new ClassReader(classFileBuffer, classFileOffset, classFileLength), output); - } - - public void classGenerateHeader(String className, PrintWriter output) throws IOException { - ClassReader reader = findClass(className); - if (reader == null) { - throw new IOException(); - } - classGenerateHeader(reader, output); - } - - public void classGenerateHeader(InputStream input, PrintWriter output) throws IOException { - classGenerateHeader(new ClassReader(input), output); - } - - public Path[] getClassPaths() { - return classPaths; - } - - public void setClassPaths(Path[] classPaths) { - Objects.requireNonNull(classPaths); - this.classPaths = classPaths; - } - - public boolean isUseRuntimeClassPath() { - return useRuntimeClassPath; - } - - public void setUseRuntimeClassPath(boolean useRuntimeClassPath) { - this.useRuntimeClassPath = useRuntimeClassPath; - } - - public static void run(ArrayList classNames, Path outputDir , ArrayList cps) throws IOException { - HeaderGenerator generator = new HeaderGenerator(cps.toArray(new Path[0])); - if (Files.notExists(outputDir)) { - Files.createDirectories(outputDir); - } - for (String name : classNames) { - ClassReader reader = generator.findClass(name); - if (reader == null) { - System.err.println("Error: Could not find class file for '" + name + "'."); - return; - } - Generator g = new Generator(); - reader.accept(g, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); - - try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter( - outputDir.resolve(name.replace('.', '_').replace("$", "__") + ".h"), - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { - generator.classGenerateHeader(g, writer); - } - } - } - -} - -final class MethodDesc { - public final boolean isStatic; - public final String descriptor; - - public MethodDesc(boolean isStatic, String descriptor) { - this.isStatic = isStatic; - this.descriptor = descriptor; - } -} - -final class Generator extends ClassVisitor { - private String className; - - private Map constants = new LinkedHashMap<>(); - private Map> methods = new LinkedHashMap>(); - - public Generator() { - super(Opcodes.ASM5); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - className = name; - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - if ((access & Opcodes.ACC_NATIVE) != 0) { - if (methods.containsKey(name)) { - methods.get(name).add(new MethodDesc((access & Opcodes.ACC_STATIC) != 0, descriptor)); - } else { - LinkedHashSet set = new LinkedHashSet<>(); - set.add(new MethodDesc((access & Opcodes.ACC_STATIC) != 0, descriptor)); - methods.put(name, set); - } - } - return null; - } - - public String getClassName() { - return className; - } - - public Map> getMethods() { - return methods; - } -} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JNIGenerator.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JNIGenerator.java new file mode 100644 index 0000000..15166b0 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JNIGenerator.java @@ -0,0 +1,236 @@ +package ch.jodersky.sbt.jni.javah; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class JNIGenerator { + + private final PrintWriter errorHandle; + private final Iterable searchPaths; + private final Path outputDir; + + public JNIGenerator(Path outputDir) { + this(outputDir, null, null); + } + + public JNIGenerator(Path outputDir, Iterable searchPaths) { + this(outputDir, searchPaths, null); + } + + public JNIGenerator(Path outputDir, Iterable searchPaths, PrintWriter errorHandle) { + Objects.requireNonNull(outputDir); + + if (searchPaths == null) { + searchPaths = Collections.singleton(RuntimeSearchPath.INSTANCE); + } + if (errorHandle == null) { + errorHandle = Utils.NOOP_WRITER; + } + + this.errorHandle = errorHandle; + this.searchPaths = searchPaths; + this.outputDir = outputDir; + } + + public void generate(ClassName name) { + Objects.requireNonNull(name); + if (Files.exists(outputDir) && !Files.isDirectory(outputDir)) { + throw new IllegalArgumentException(outputDir + "is not a directory"); + } + if (Files.notExists(outputDir)) { + try { + Files.createDirectories(outputDir); + } catch (IOException e) { + errorHandle.println("error: cannot create directory " + outputDir); + e.printStackTrace(errorHandle); + return; + } + } + Path op = outputDir.resolve(name.mangledName() + ".h"); + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(op))) { + generateTo(name, out); + } catch (Exception ex) { + errorHandle.println("error: cannot write to " + op); + ex.printStackTrace(errorHandle); + try { + Files.deleteIfExists(op); + } catch (IOException ignored) { + } + } + } + + public void generateTo(ClassName name, Writer writer) throws IOException { + Objects.requireNonNull(name); + Objects.requireNonNull(writer); + + ClassMetaInfo meta = new ClassMetaInfo(); + { + Path f = search(name); + if (f == null) { + errorHandle.println("Not found class " + name); + return; + } + + try (InputStream in = Files.newInputStream(f)) { + ClassReader reader = new ClassReader(in); + reader.accept(meta, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } catch (IOException e) { + errorHandle.println("error: cannot open class file of " + name); + e.printStackTrace(errorHandle); + errorHandle.flush(); + throw e; + } + } + + PrintWriter out = writer instanceof PrintWriter ? (PrintWriter) writer : new PrintWriter(writer); + out.println("/* DO NOT EDIT THIS FILE - it is machine generated */"); + out.println("#include "); + out.println("/* Header for class " + name.mangledName() + " */"); + out.println(); + out.println("#ifndef _Included_" + name.mangledName()); + out.println("#define _Included_" + name.mangledName()); + out.println("#ifdef __cplusplus"); + out.println("extern \"C\" {"); + out.println("#endif"); + + for (Constant constant : meta.constants) { + String cn = name.mangledName() + "_" + constant.mangledName(); + out.println("#undef " + cn); + out.println("#define " + cn + " " + constant.valueToString()); + } + + for (NativeMethod method : meta.methods) { + String ret = mapTypeToNative(method.type().getReturnType()); + List args = new ArrayList<>(); + args.add("JNIEnv *"); + args.add(method.isStatic() ? "jclass" : "jobject"); + args.addAll(Arrays.asList(mapArgsTypeToNative(method.type()))); + + String methodName = + "Java_" + name.mangledName() + "_" + (meta.isOverloadMethod(method) ? method.longMangledName() : method.mangledName()); + + out.println("/*"); + out.println(" * Class: " + name.mangledName()); + out.println(" * Method: " + method.mangledName()); + out.println(" * Signature: " + Utils.escape(method.type().toString())); + out.println(" */"); + out.println("JNIEXPORT " + ret + " JNICALL " + methodName); + out.println(" (" + String.join(", ", args) + ");"); + out.println(); + } + + out.println("#ifdef __cplusplus"); + out.println("}"); + out.println("#endif"); + out.println("#endif"); + + } + + private Path search(ClassName name) { + return SearchPath.searchFrom(searchPaths, name); + } + + private String mapTypeToNative(Type type) { + Objects.requireNonNull(type); + String tpe = type.toString(); + if (tpe.startsWith("(")) { + throw new IllegalArgumentException(); + } + + switch (tpe) { + case "Z": + return "jboolean"; + case "B": + return "jbyte"; + case "C": + return "jchar"; + case "S": + return "jshort"; + case "I": + return "jint"; + case "J": + return "jlong"; + case "F": + return "jfloat"; + case "D": + return "jdouble"; + case "V": + return "void"; + case "Ljava/lang/Class;": + return "jclass"; + case "Ljava/lang/String;": + return "jstring"; + case "Ljava/lang/Throwable;": + return "jthrowable"; + case "[Z": + return "jbooleanArray"; + case "[B": + return "jbyteArray"; + case "[C": + return "jcharArray"; + case "[S": + return "jshortArray"; + case "[I": + return "jintArray"; + case "[J": + return "jlongArray"; + case "[F": + return "jfloatArray"; + case "[D": + return "jdoubleArray"; + } + + if (tpe.startsWith("[")) { + return "jobjectArray"; + } + + if (tpe.startsWith("L") && tpe.endsWith(";")) { + ClassName n = ClassName.of(tpe.substring(1, tpe.length() - 1).replace('/', '.')); + if (isThrowable(n)) { + return "jthrowable"; + } else { + return "jobject"; + } + } + throw new IllegalArgumentException("Unknown type: " + type); + } + + private String[] mapArgsTypeToNative(Type methodType) { + Objects.requireNonNull(methodType); + if (!Utils.METHOD_TYPE_PATTERN.matcher(methodType.toString()).matches()) { + throw new IllegalArgumentException(methodType + " is not a method type"); + } + Type[] args = methodType.getArgumentTypes(); + String[] ans = new String[args.length]; + for (int i = 0; i < args.length; i++) { + ans[i] = mapTypeToNative(args[i]); + } + return ans; + } + + private boolean isThrowable(ClassName name) { + if (name == null) { + return false; + } + switch (name.className()) { + case "java.lang.Throwable": + case "java.lang.Error": + case "java.lang.Exception": + return true; + case "java.lang.Object": + return false; + } + + try (InputStream in = Files.newInputStream(search(name))) { + return isThrowable(Utils.superClassOf(new ClassReader(in))); + } catch (Exception ignored) { + errorHandle.println("warning: class " + name + " not found"); + return false; + } + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JavahTask.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JavahTask.java new file mode 100644 index 0000000..23627a4 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/JavahTask.java @@ -0,0 +1,81 @@ +package ch.jodersky.sbt.jni.javah; + +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.Path; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +public final class JavahTask { + private final List searchPaths = new LinkedList<>(); + private Path outputDir; + private PrintWriter errorHandle = new PrintWriter(System.err, true); + private final List classes = new LinkedList<>(); + + public void run() { + Objects.requireNonNull(outputDir, "outputDir"); + JNIGenerator generator = new JNIGenerator(outputDir, searchPaths, errorHandle); + for (ClassName cls : classes) { + try { + generator.generate(cls); + } catch (Exception ex) { + ex.printStackTrace(errorHandle); + } + } + } + + public void addClass(ClassName name) { + Objects.requireNonNull(name); + classes.add(name); + } + + public void addClass(String name) { + Objects.requireNonNull(name); + classes.add(ClassName.of(name)); + } + + public void addClasses(Iterable i) { + Objects.requireNonNull(i); + i.forEach(c -> classes.add(ClassName.of(c))); + } + + public void addRuntimeSearchPath() { + searchPaths.add(RuntimeSearchPath.INSTANCE); + } + + public void addSearchPath(SearchPath searchPath) { + Objects.requireNonNull(searchPath); + searchPaths.add(searchPath); + } + + public void addClassPath(Path classPath) { + Objects.requireNonNull(classPath); + searchPaths.add(new ClassPath(classPath)); + } + + public void addModulePath(Path modulePath) { + Objects.requireNonNull(modulePath); + searchPaths.add(new ModulePath(modulePath)); + } + + public Path getOutputDir() { + return outputDir; + } + + public void setOutputDir(Path outputDir) { + this.outputDir = outputDir; + } + + public PrintWriter getErrorHandle() { + return errorHandle; + } + + public void setErrorHandle(Writer errorHandle) { + if (errorHandle instanceof PrintWriter || errorHandle == null) { + this.errorHandle = (PrintWriter) errorHandle; + } else { + this.errorHandle = new PrintWriter(errorHandle); + } + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ModulePath.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ModulePath.java new file mode 100644 index 0000000..59b0d99 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/ModulePath.java @@ -0,0 +1,50 @@ +package ch.jodersky.sbt.jni.javah; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ModulePath implements SearchPath { + private final Path path; + private List roots; + + public ModulePath(Path path) { + Objects.requireNonNull(path); + path = path.toAbsolutePath(); + this.path = path; + if (Files.notExists(path) || !Files.isDirectory(path)) { + roots = Collections.emptyList(); + } else { + try { + roots = Files.list(path) + .map(Path::toAbsolutePath) + .filter(Files::isRegularFile) + .filter(p -> { + String n = p.getFileName().toString().toLowerCase(); + return n.endsWith(".jar") || n.endsWith(".zip") || n.endsWith(".jmod"); + }) + .map(Utils::classPathRoot) + .filter(Objects::nonNull) + .flatMap(p -> SearchPath.multiReleaseRoots(p).stream()) + .collect(Collectors.toList()); + } catch (IOException e) { + roots = Collections.emptyList(); + } + } + } + + @Override + public Path search(ClassName name) { + Objects.requireNonNull(name); + return SearchPath.searchFromRoots(roots, name); + } + + @Override + public String toString() { + return "ModulePath[" + path + "]"; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/NativeMethod.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/NativeMethod.java new file mode 100644 index 0000000..aee2c8d --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/NativeMethod.java @@ -0,0 +1,90 @@ +package ch.jodersky.sbt.jni.javah; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.Objects; +import java.util.regex.Matcher; + +public final class NativeMethod { + private final int access; + private final String name; + private final Type type; + private final String mangledName; + private final String longMangledName; + + public static NativeMethod of(String name, String descriptor) { + return NativeMethod.of(0, name, descriptor); + } + + public static NativeMethod of(String name, Type type) { + return NativeMethod.of(0, name, type); + } + + public static NativeMethod of(int access, String name, String descriptor) { + Objects.requireNonNull(name); + Objects.requireNonNull(descriptor); + return NativeMethod.of(access, name, Type.getType(descriptor)); + } + + public static NativeMethod of(int access, String name, Type type) { + Objects.requireNonNull(name); + Objects.requireNonNull(type); + if (!Utils.METHOD_NAME_PATTERN.matcher(name).matches()) { + throw new IllegalArgumentException(String.format("\"%s\" is not a qualified method name", name)); + } + Matcher m = Utils.METHOD_TYPE_PATTERN.matcher(type.toString()); + if (!m.matches()) { + throw new IllegalArgumentException(String.format("\"%s\" is not a method type", type)); + } + return new NativeMethod(access, name, type, m.group("args")); + } + + private NativeMethod(int access, String name, Type type, String arguments) { + this.access = access; + this.name = name; + this.type = type; + this.mangledName = Utils.mangleName(name); + this.longMangledName = mangledName + "__" + Utils.mangleName(arguments); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NativeMethod)) { + return false; + } + NativeMethod that = (NativeMethod) o; + return name.equals(that.name) && type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(name, type); + } + + @Override + public String toString() { + return String.format("NativeMethod[name=%s, type=%s}", name, type); + } + + public String name() { + return name; + } + + public Type type() { + return type; + } + + public String mangledName() { + return mangledName; + } + + public String longMangledName() { + return longMangledName; + } + + public boolean isStatic() { + return (access & Opcodes.ACC_STATIC) != 0; + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/RuntimeSearchPath.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/RuntimeSearchPath.java new file mode 100644 index 0000000..a43759c --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/RuntimeSearchPath.java @@ -0,0 +1,44 @@ +package ch.jodersky.sbt.jni.javah; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.util.Collections; +import java.util.Objects; + +public class RuntimeSearchPath implements SearchPath { + public static final RuntimeSearchPath INSTANCE = new RuntimeSearchPath(); + + private RuntimeSearchPath() { + + } + + @Override + public Path search(ClassName name) { + Objects.requireNonNull(name); + URI uri = null; + try { + Class cls = Class.forName(name.className()); + uri = cls.getResource(name.simpleName() + ".class").toURI(); + return Paths.get(uri); + } catch (FileSystemNotFoundException ex) { + if (uri == null) { + return null; + } + try { + return FileSystems.newFileSystem(uri, Collections.emptyMap()).getPath("/", name.relativePath()); + } catch (IOException | NullPointerException ignored) { + } + } catch (Exception ignored) { + } + return null; + } + + public static Path searchClass(String name) { + return INSTANCE.search(name); + } + + public static Path searchClass(ClassName name) { + return INSTANCE.search(name); + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/SearchPath.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/SearchPath.java new file mode 100644 index 0000000..7c6b43b --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/SearchPath.java @@ -0,0 +1,90 @@ +package ch.jodersky.sbt.jni.javah; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +public interface SearchPath { + Path search(ClassName name); + + default Path search(String fullName) { + Objects.requireNonNull(fullName); + return search(ClassName.of(fullName)); + } + + static Path searchFrom(Iterable searchPaths, ClassName name) { + Objects.requireNonNull(searchPaths); + Objects.requireNonNull(name); + + for (SearchPath searchPath : searchPaths) { + if (searchPath == null) { + continue; + } + Path p = searchPath.search(name); + if (p != null) { + return p; + } + } + return null; + } + + static Path searchFromRoots(Iterable roots, ClassName name) { + Objects.requireNonNull(roots); + Objects.requireNonNull(name); + for (Path root : roots) { + if (root == null || !Files.isDirectory(root)) { + continue; + } + + Path p = root.resolve(name.relativePath()); + if (Files.isRegularFile(p)) { + return p; + } + if (Files.isSymbolicLink(p)) { + try { + p = Files.readSymbolicLink(p); + if (Files.isRegularFile(p)) { + return p; + } + } catch (IOException ignored) { + } + } + } + + return null; + } + + static List multiReleaseRoots(Path root) { + Objects.requireNonNull(root); + if (!Files.isDirectory(root)) { + return Collections.emptyList(); + } + boolean isMultiRelease = false; + try (InputStream in = Files.newInputStream(root.resolve("META-INF").resolve("MANIFEST.MF"))) { + isMultiRelease = "true".equals(new Manifest(in).getMainAttributes().getValue("Multi-Release")); + } catch (IOException | NullPointerException ignored) { + } + + if (isMultiRelease) { + Path base = root.resolve("META-INF").resolve("versions"); + if (Files.isDirectory(base)) { + try { + List list = Files.list(base) + .map(Path::toAbsolutePath) + .filter(Files::isDirectory) + .filter(p -> Utils.MULTI_RELEASE_VERSIONS.contains(p.getFileName().toString())) + .sorted(Comparator.comparing((Path p) -> Integer.parseInt(p.getFileName().toString())).reversed()) + .collect(Collectors.toCollection(LinkedList::new)); + list.add(root); + return Collections.unmodifiableList(list); + } catch (IOException ignored) { + } + } + } + return Collections.singletonList(root); + } +} diff --git a/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Utils.java b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Utils.java new file mode 100644 index 0000000..61e7a96 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/Utils.java @@ -0,0 +1,133 @@ +package ch.jodersky.sbt.jni.javah; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +class Utils { + public static final int MAX_SUPPORTED_VERSION = 13; + + public static final List MULTI_RELEASE_VERSIONS = + IntStream.rangeClosed(9, MAX_SUPPORTED_VERSION).mapToObj(Integer::toString).collect(Collectors.toList()); + + public static final Pattern SIMPLE_NAME_PATTERN = Pattern.compile("[^.;\\[/]+"); + public static final Pattern FULL_NAME_PATTERN = + Pattern.compile("[^.;\\[/]+(\\.[^.;\\[/]+)*"); + + public static final Pattern METHOD_NAME_PATTERN = Pattern.compile("()|()|([^.;\\[/<>]+)"); + public static final Pattern METHOD_TYPE_PATTERN = + Pattern.compile("\\((?(\\[*([BCDFIJSZ]|L[^.;\\[/]+(/[^.;\\\\\\[/]+)*;))*)\\)(?\\[*([BCDFIJSZV]|L[^.;\\[/]+(/[^.;\\[/]+)*;))"); + + public static final PrintWriter NOOP_WRITER = new PrintWriter(new Writer() { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + } + + @Override + public void flush() throws IOException { + } + + @Override + public void close() throws IOException { + } + }); + + public static String mangleName(String name) { + StringBuilder builder = new StringBuilder(name.length() * 2); + int len = name.length(); + for (int i = 0; i < len; i++) { + char ch = name.charAt(i); + if (ch == '.') { + builder.append('_'); + } else if (ch == '_') { + builder.append("_1"); + } else if (ch == ';') { + builder.append("_2"); + } else if (ch == '[') { + builder.append("_3"); + } else if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && (ch <= 'Z'))) { + builder.append(ch); + } else { + builder.append(String.format("_0%04x", (int) ch)); + } + } + return builder.toString(); + } + + public static String escape(String unicode) { + Objects.requireNonNull(unicode); + int len = unicode.length(); + StringBuilder builder = new StringBuilder(len); + for (int i = 0; i < len; i++) { + char ch = unicode.charAt(i); + if (ch >= ' ' && ch <= '~') { + builder.append(ch); + } else { + builder.append(String.format("\\u%04x", (int) ch)); + } + } + return builder.toString(); + } + + public static Path classPathRoot(Path p) { + Objects.requireNonNull(p); + p = p.toAbsolutePath(); + + if (Files.notExists(p)) { + return null; + } + if (Files.isDirectory(p)) { + return p; + } + + try { + FileSystem fs = FileSystems.newFileSystem(p, (ClassLoader) null); + String name = p.getFileName().toString().toLowerCase(); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return fs.getPath("/"); + } + if (name.endsWith(".jmod")) { + return fs.getPath("/", "classes"); + } + + fs.close(); + } catch (IOException ignored) { + return null; + } + return null; + } + + public static ClassName superClassOf(ClassReader reader) { + Objects.requireNonNull(reader); + class V extends ClassVisitor { + V() { + super(Opcodes.ASM6); + } + + ClassName superName = null; + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (superName != null) { + this.superName = ClassName.of(superName.replace('/', '.')); + } + } + } + V v = new V(); + reader.accept(v, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + return v.superName; + } +} diff --git a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala index a9832a5..0c068ed 100644 --- a/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala +++ b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala @@ -61,11 +61,12 @@ object JniJavah extends AutoPlugin { log.info("Headers will be generated to " + out.getAbsolutePath) } - import scala.collection.JavaConverters._ - - ch.jodersky.sbt.jni.javah.HeaderGenerator.run(new util.ArrayList[String](classes.asJava), - Paths.get(out.getAbsolutePath), new util.ArrayList[Path](jcp.map(_.toPath).asJava) - ) + val task = new ch.jodersky.sbt.jni.javah.JavahTask + classes.foreach(task.addClass(_)) + jcp.map(_.toPath).foreach(task.addClassPath(_)) + task.addRuntimeSearchPath() + task.setOutputDir(Paths.get(out.getAbsolutePath)) + task.run() out } -- cgit v1.2.3