From 221b3843a5a947d803507b8e61778ebfea072390 Mon Sep 17 00:00:00 2001 From: Kamyar Date: Sun, 24 Nov 2019 14:39:23 -0500 Subject: Incorporate gjavah internally instead of deprecated javah (#33) incorporate `gjavah` instead of deprecated javah --- README.md | 5 +- build.sbt | 6 +- .../ch/jodersky/sbt/jni/javah/HeaderGenerator.java | 397 +++++++++++++++++++++ .../ch/jodersky/sbt/jni/plugins/JniJavah.scala | 27 +- .../sbt-jni/simple/project/ScriptedHelper.scala | 2 +- .../sbt-jni/simple/project/build.properties | 2 +- project/build.properties | 2 +- 7 files changed, 419 insertions(+), 22 deletions(-) create mode 100644 plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java diff --git a/README.md b/README.md index 4556ff3..79d23d2 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,10 @@ Note that most plugins are enabled in projects by default. Disabling their funct |--------------------------------|---------------| | automatic, for all projects | [JniJavah.scala](plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala)| -This plugin wraps the JDK `javah` command. +This plugin wraps the JDK `javah` command [^1]. + +[^1]: Glavo's [gjavah](https://github.com/Glavo/gjavah) is actually used, since `javah` has been +removed from the JDK since version 1.10. Run `sbt-javah` to generate C header files with prototypes for any methods marked as native. E.g. the following scala class diff --git a/build.sbt b/build.sbt index 4260980..7563831 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ import scala.sys.process._ -val scalaVersions = Seq("2.13.0", "2.12.8", "2.11.12") -val macrosParadiseVersion = "2.1.0" +val scalaVersions = Seq("2.13.1", "2.12.10", "2.11.12") +val macrosParadiseVersion = "2.1.1" // version is derived from latest git tag version in ThisBuild := ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim @@ -54,7 +54,7 @@ lazy val plugin = (project in file("plugin")) .settings( name := "sbt-jni", publishMavenStyle := false, - libraryDependencies += "org.ow2.asm" % "asm" % "6.0", + libraryDependencies += "org.ow2.asm" % "asm" % "6.2.1", // make project settings available to source sourceGenerators in Compile += Def.task { val src = s"""|/* Generated by sbt */ 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 new file mode 100644 index 0000000..a26aa20 --- /dev/null +++ b/plugin/src/main/java/ch/jodersky/sbt/jni/javah/HeaderGenerator.java @@ -0,0 +1,397 @@ +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/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala b/plugin/src/main/scala/ch/jodersky/sbt/jni/plugins/JniJavah.scala index e313112..a9832a5 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 @@ -1,11 +1,14 @@ package ch.jodersky.sbt.jni package plugins +import java.nio.file.{Path, Paths} + import collection.JavaConverters._ import util.BytecodeUtil +import java.util + import sbt._ import sbt.Keys._ -import sys.process._ /** Adds `javah` header-generation functionality to projects. */ object JniJavah extends AutoPlugin { @@ -51,25 +54,19 @@ object JniJavah extends AutoPlugin { (compile in Compile).value; Seq((classDirectory in Compile).value) } - val cp = jcp.mkString(sys.props("path.separator")) val log = streams.value.log val classes = (javahClasses in javah).value - if (!classes.isEmpty) { + if (classes.nonEmpty) { log.info("Headers will be generated to " + out.getAbsolutePath) } - for (clazz <- classes) { - log.info("Generating header for " + clazz) - val parts = Seq( - "javah", - "-d", out.getAbsolutePath, - "-classpath", cp, - clazz - ) - val cmd = parts.mkString(" ") - val ev = Process(cmd) ! log - if (ev != 0) sys.error(s"Error occured running javah. Exit code: ${ev}") - } + + 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) + ) + out } ) diff --git a/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala b/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala index d5d1d6e..4fc2c8a 100644 --- a/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala +++ b/plugin/src/sbt-test/sbt-jni/simple/project/ScriptedHelper.scala @@ -8,7 +8,7 @@ object ScriptedHelper extends AutoPlugin { override def projectSettings = Seq( scalacOptions ++= Seq("-feature", "-deprecation"), - crossScalaVersions := Seq("2.13.0", "2.12.8", "2.11.12"), + crossScalaVersions := Seq("2.13.1", "2.12.10", "2.11.12"), scalaVersion := crossScalaVersions.value.head ) diff --git a/plugin/src/sbt-test/sbt-jni/simple/project/build.properties b/plugin/src/sbt-test/sbt-jni/simple/project/build.properties index c0bab04..6adcdc7 100644 --- a/plugin/src/sbt-test/sbt-jni/simple/project/build.properties +++ b/plugin/src/sbt-test/sbt-jni/simple/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.3.3 diff --git a/project/build.properties b/project/build.properties index c0bab04..6adcdc7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.3.3 -- cgit v1.2.3