aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGlavo <zjx001202@126.com>2019-12-06 03:42:00 +0800
committerGlavo <zjx001202@126.com>2019-12-06 03:42:00 +0800
commite48c0668029849af07d1966729c5b703e89b1d00 (patch)
treefec378e2c6e52d86e601f0df45ba13728003fa2b
parent6fd5804e3f2c337839efcf5c10426faed1d8b1af (diff)
downloadgjavah-e48c0668029849af07d1966729c5b703e89b1d00.zip
gjavah-e48c0668029849af07d1966729c5b703e89b1d00.tar.gz
gjavah-e48c0668029849af07d1966729c5b703e89b1d00.tar.bz2
update
-rw-r--r--build.gradle34
-rw-r--r--src/main/java/module-info.java2
-rw-r--r--src/main/java/org/glavo/javah/ClassMetaInfo.java46
-rw-r--r--src/main/java/org/glavo/javah/ClassName.java88
-rw-r--r--src/main/java/org/glavo/javah/ClassPath.java30
-rw-r--r--src/main/java/org/glavo/javah/Constant.java83
-rw-r--r--src/main/java/org/glavo/javah/JNIGenerator.java315
-rw-r--r--src/main/java/org/glavo/javah/JavahTask.java163
-rw-r--r--src/main/java/org/glavo/javah/Main.java142
-rw-r--r--src/main/java/org/glavo/javah/ModulePath.java40
-rw-r--r--src/main/java/org/glavo/javah/NativeMethod.java92
-rw-r--r--src/main/java/org/glavo/javah/RuntimeClassPath.java41
-rw-r--r--src/main/java/org/glavo/javah/RuntimeSearchPath.java44
-rw-r--r--src/main/java/org/glavo/javah/SearchPath.java87
-rw-r--r--src/main/java/org/glavo/javah/Utils.java271
-rw-r--r--src/test/java/org/glavo/javah/ClassMetaInfoTest.java54
-rw-r--r--src/test/java/org/glavo/javah/NativeMethodTests.java34
-rw-r--r--src/test/java/org/glavo/javah/RegexTests.java73
-rw-r--r--src/test/java/org/glavo/javah/RuntimeSearchPathTests.java31
19 files changed, 1142 insertions, 528 deletions
diff --git a/build.gradle b/build.gradle
index e12cda7..600a966 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,14 +16,44 @@ jar {
)
}
+tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+}
+
+compileTestJava {
+ moduleOptions {
+ addModules = [
+ 'org.junit.jupiter.api'
+ ]
+ addReads = [
+ 'org.glavo.javah': 'org.junit.jupiter.api'
+ ]
+ }
+}
+
+test {
+ testLogging.showStandardStreams = true
+ useJUnitPlatform()
+ moduleOptions {
+ runOnClasspath = true
+ }
+}
+
repositories {
mavenCentral()
}
dependencies {
+ // https://mvnrepository.com/artifact/commons-cli/commons-cli
+ implementation group: 'info.picocli', name: 'picocli', version: '4.1.1'
+
// https://mvnrepository.com/artifact/org.ow2.asm/asm
- compile group: 'org.ow2.asm', name: 'asm', version: '7.2'
+ implementation group: 'org.ow2.asm', name: 'asm', version: '7.2'
- testCompile group: 'junit', name: 'junit', version: '4.12'
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.5.2'
}
+task copyDependencies(type: Copy) {
+ from configurations.default
+ into 'build/libs'
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 27cf6ae..bac1bc3 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,8 @@
module org.glavo.javah {
requires org.objectweb.asm;
+ requires info.picocli;
requires jdk.zipfs;
exports org.glavo.javah;
+ opens org.glavo.javah to info.picocli;
} \ No newline at end of file
diff --git a/src/main/java/org/glavo/javah/ClassMetaInfo.java b/src/main/java/org/glavo/javah/ClassMetaInfo.java
new file mode 100644
index 0000000..bc3ffcc
--- /dev/null
+++ b/src/main/java/org/glavo/javah/ClassMetaInfo.java
@@ -0,0 +1,46 @@
+package org.glavo.javah;
+
+import org.objectweb.asm.*;
+
+import java.util.*;
+
+class ClassMetaInfo extends ClassVisitor {
+ final List<Constant> constants = new LinkedList<>();
+ final List<NativeMethod> methods = new LinkedList<>();
+ final Map<String, Integer> counts = new HashMap<>();
+
+ ClassName superClassName;
+ ClassName name;
+
+ public ClassMetaInfo() {
+ super(Opcodes.ASM7);
+ }
+
+ @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/src/main/java/org/glavo/javah/ClassName.java b/src/main/java/org/glavo/javah/ClassName.java
new file mode 100644
index 0000000..4305036
--- /dev/null
+++ b/src/main/java/org/glavo/javah/ClassName.java
@@ -0,0 +1,88 @@
+package org.glavo.javah;
+
+import java.util.Objects;
+
+import static org.glavo.javah.Utils.*;
+
+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 && !FULL_NAME_PATTERN.matcher(moduleName).matches()) {
+ throw new IllegalArgumentException("Illegal module name: " + moduleName);
+ }
+ if (!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 = 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/src/main/java/org/glavo/javah/ClassPath.java b/src/main/java/org/glavo/javah/ClassPath.java
index 2aea039..3e59c59 100644
--- a/src/main/java/org/glavo/javah/ClassPath.java
+++ b/src/main/java/org/glavo/javah/ClassPath.java
@@ -1,23 +1,41 @@
package org.glavo.javah;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import static org.glavo.javah.Utils.*;
+
public class ClassPath implements SearchPath {
private final Path path;
- private final List<Path> searchPaths;
+ private final List<Path> roots;
public ClassPath(Path path) {
Objects.requireNonNull(path);
- this.path = path;
- searchPaths = Utils.getPathsFrom(path);
+ this.path = path.toAbsolutePath();
+
+ Path root = classPathRoot(path);
+ roots = root == null ? Collections.emptyList() : SearchPath.multiReleaseRoots(root);
+ }
+
+ @Override
+ public Path search(ClassName name) {
+ Objects.requireNonNull(name);
+ return SearchPath.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 Path searchClass(String className) {
- Objects.requireNonNull(className);
- return Utils.searchFrom(searchPaths, className);
+ public int hashCode() {
+ return path.hashCode();
}
@Override
diff --git a/src/main/java/org/glavo/javah/Constant.java b/src/main/java/org/glavo/javah/Constant.java
new file mode 100644
index 0000000..1e734ba
--- /dev/null
+++ b/src/main/java/org/glavo/javah/Constant.java
@@ -0,0 +1,83 @@
+package org.glavo.javah;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import static org.glavo.javah.Utils.*;
+
+public final class Constant {
+ private static final List<Class<?>> 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 (!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 = 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/src/main/java/org/glavo/javah/JNIGenerator.java b/src/main/java/org/glavo/javah/JNIGenerator.java
index c1a9d40..bec2ea9 100644
--- a/src/main/java/org/glavo/javah/JNIGenerator.java
+++ b/src/main/java/org/glavo/javah/JNIGenerator.java
@@ -1,143 +1,238 @@
package org.glavo.javah;
-import org.objectweb.asm.*;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Type;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.UncheckedIOException;
+import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-public class JNIGenerator extends ClassVisitor {
- public static final String FILE_HEADER =
- "/* DO NOT EDIT THIS FILE - it is machine generated */\n" +
- "#include <jni.h>\n";
+import static org.glavo.javah.Utils.*;
- public static final String FILE_END = "\n";
+public class JNIGenerator {
- private final PrintWriter output;
- private final Path classFile;
+ private final PrintWriter errorHandle;
+ private final Iterable<SearchPath> searchPaths;
+ private final Path outputDir;
- private String className;
+ public JNIGenerator(Path outputDir) {
+ this(outputDir, null, null);
+ }
+
+ public JNIGenerator(Path outputDir, Iterable<SearchPath> searchPaths) {
+ this(outputDir, searchPaths, null);
+ }
- private Map<String, Integer> methodNameCount = new HashMap<>();
- private LinkedList<NativeMethod> methods = new LinkedList<>();
- private LinkedList<ConstantField> constants = new LinkedList<>();
+ public JNIGenerator(Path outputDir, Iterable<SearchPath> searchPaths, PrintWriter errorHandle) {
+ Objects.requireNonNull(outputDir);
- public JNIGenerator(PrintWriter output, Path classFile) {
- super(Opcodes.ASM7);
- Objects.requireNonNull(output);
- Objects.requireNonNull(classFile);
- this.output = output;
- this.classFile = classFile;
+ if (searchPaths == null) {
+ searchPaths = Collections.singleton(RuntimeSearchPath.INSTANCE);
+ }
+ if (errorHandle == null) {
+ errorHandle = NOOP_WRITER;
+ }
+
+ this.errorHandle = errorHandle;
+ this.searchPaths = searchPaths;
+ this.outputDir = outputDir;
}
- public void generate() {
- ClassReader cls = null;
- try {
- cls = new ClassReader(Files.readAllBytes(classFile));
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- cls.accept(this, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
-
- String name = Utils.encode(className).replace('/', '_');
- String guard = "_Included_" + className;
-
- output.println("/* Header for class " + className + " */\n\n");
- output.println("#ifndef " + guard);
- output.println("#define " + guard);
- output.println("#ifdef __cplusplus");
- output.println("extern \"C\" {");
- output.println("#endif");
-
- for (ConstantField constant : constants) {
- String cm = name + "_" + Utils.encode(constant.name);
- String value;
- if (constant.value instanceof Float) {
- value = constant.value.toString() + "f";
- } else if (constant.value instanceof Long) {
- value = constant.value.toString() + "i64";
- } else if (constant.value instanceof Double) {
- value = constant.value.toString();
- } else {
- value = constant.value.toString() + "L";
+ 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;
}
- output.println("#undef " + cm);
- output.println("#define " + cm + " " + value);
}
- for (NativeMethod method : methods) {
- String mm = "Java_" + name + "_" + Utils.encode(method.name);
- if (methodNameCount.getOrDefault(method.name, 1) > 1) {
- mm = mm + "__" + Utils.mangledArgSignature(method.type);
+ 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) {
}
- String r = Utils.mapToNativeType(method.type.getReturnType());
- String args = Stream.concat(Stream.of("JNIEnv *", method.isStatic ? "jclass" : "jobject"),
- Arrays.stream(method.type.getArgumentTypes()).map(Utils::mapToNativeType))
- .collect(Collectors.joining(","));
-
- output.println("/*");
- output.println(" * Class: " + name);
- output.println(" * Method: " + mm);
- output.println(" * Signature: " + method.type);
- output.println(" */");
- output.println("JNIEXPORT " + r + " JNICALL " + mm);
- output.println(" (" + args + ");");
- output.println();
- }
-
- output.println("#ifdef __cplusplus");
- output.println("}");
- output.println("#endif");
- output.println("#endif");
+ }
}
- @Override
- public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
- className = name;
- }
+ 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;
+ }
+ }
- @Override
- public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
- if ((access & Opcodes.ACC_NATIVE) != 0) {
- methodNameCount.put(name, methodNameCount.getOrDefault(name, 0) + 1);
- methods.add(new NativeMethod(name, Type.getType(descriptor), (access & Opcodes.ACC_STATIC) != 0));
+ PrintWriter out = writer instanceof PrintWriter ? (PrintWriter) writer : new PrintWriter(writer);
+ out.println("/* DO NOT EDIT THIS FILE - it is machine generated */");
+ out.println("#include <jni.h>");
+ 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());
}
- return null;
- }
- @Override
- public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
- if (value != null && !(value instanceof String)) {
- constants.add(new ConstantField(name, Type.getType(descriptor), value));
+ for (NativeMethod method : meta.methods) {
+ String ret = mapTypeToNative(method.type().getReturnType());
+ List<String> 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: " + escape(method.type().toString()));
+ out.println(" */");
+ out.println("JNIEXPORT " + ret + " JNICALL " + methodName);
+ out.println(" (" + String.join(", ", args) + ");");
+ out.println();
}
- return null;
+
+ out.println("#ifdef __cplusplus");
+ out.println("}");
+ out.println("#endif");
+ out.println("#endif");
+
+ }
+
+ private Path search(ClassName name) {
+ return SearchPath.searchFrom(searchPaths, name);
}
- private static class NativeMethod {
- String name;
- Type type;
- boolean isStatic;
+ 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);
+ }
- public NativeMethod(String name, Type type, boolean isStatic) {
- this.name = name;
- this.type = type;
- this.isStatic = isStatic;
+ private String[] mapArgsTypeToNative(Type methodType) {
+ Objects.requireNonNull(methodType);
+ if (!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 static class ConstantField {
- String name;
- Type type;
- Object value;
+ 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;
+ }
- public ConstantField(String name, Type type, Object value) {
- this.name = name;
- this.type = type;
- this.value = value;
+ try (InputStream in = Files.newInputStream(search(name))) {
+ return isThrowable(superClassOf(new ClassReader(in)));
+ } catch (Exception ignored) {
+ errorHandle.println("warning: class " + name + " not found");
+ return false;
}
}
}
diff --git a/src/main/java/org/glavo/javah/JavahTask.java b/src/main/java/org/glavo/javah/JavahTask.java
index 2da9400..8e239ed 100644
--- a/src/main/java/org/glavo/javah/JavahTask.java
+++ b/src/main/java/org/glavo/javah/JavahTask.java
@@ -1,159 +1,82 @@
package org.glavo.javah;
-import java.io.File;
-import java.io.IOException;
+import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
-import java.util.stream.Stream;
-public class JavahTask {
- private PrintWriter errorHandle = new PrintWriter(System.err);
- private boolean outputToSignalFile = false;
- private Path outputPath = null;
-
- private final List<SearchPath> searchPaths = new ArrayList<>();
- private final List<String> classList = new ArrayList<>();
-
- public void run() throws IOException {
- if (outputPath == null) {
- outputPath = Paths.get(".").toAbsolutePath();
- outputToSignalFile = false;
- }
-
- if (outputToSignalFile) {
- try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputPath))) {
- writer.write(JNIGenerator.FILE_HEADER);
- classList.stream()
- .map(this::search)
- .filter(Objects::nonNull)
- .filter(Files::isReadable)
- .map(p -> new JNIGenerator(writer, p))
- .forEachOrdered(JNIGenerator::generate);
-
- writer.write(JNIGenerator.FILE_END);
- }
- } else {
- for (String c : classList) {
- Path p = search(c);
- if (p == null) {
- continue;
- }
- try (PrintWriter output = new PrintWriter(Files.newBufferedWriter(
- outputPath.resolve(Utils.encode(c).replace('.', '_') + ".h")))) {
- output.write(JNIGenerator.FILE_HEADER);
- new JNIGenerator(output, p).generate();
- output.write(JNIGenerator.FILE_END);
- }
+public final class JavahTask {
+ private final List<SearchPath> searchPaths = new LinkedList<>();
+ private Path outputDir;
+ private PrintWriter errorHandle = new PrintWriter(System.err, true);
+ private final List<ClassName> 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);
}
}
}
- private Path search(String className) {
- for (SearchPath searchPath : searchPaths) {
- Path c = searchPath.searchClass(className);
- if (c != null) {
- return c;
- }
- }
- errorHandle.println("class" + className + " not found");
- return null;
+ public void addClass(ClassName name) {
+ Objects.requireNonNull(name);
+ classes.add(name);
}
- //
- // Getters and Setters
- //
+ public void addClass(String name) {
+ Objects.requireNonNull(name);
+ classes.add(ClassName.of(name));
+ }
- public void addSearchPath(SearchPath searchPath) {
- Objects.requireNonNull(searchPath);
- searchPaths.add(searchPath);
+ public void addClasses(Iterable<String> i) {
+ Objects.requireNonNull(i);
+ i.forEach(c -> classes.add(ClassName.of(c)));
}
- public void addModulePath(Path modulePath) {
- Objects.requireNonNull(modulePath);
- searchPaths.add(new ModulePath(modulePath));
+ public void addRuntimeSearchPath() {
+ searchPaths.add(RuntimeSearchPath.INSTANCE);
}
- public void addModulePaths(String modulePaths) {
- Objects.requireNonNull(modulePaths);
- Arrays.stream(modulePaths.split(File.pathSeparator))
- .filter(s -> !"".equals(s))
- .map(Paths::get)
- .filter(Files::isDirectory)
- .forEachOrdered(this::addModulePath);
+ public void addSearchPath(SearchPath searchPath) {
+ Objects.requireNonNull(searchPath);
+ searchPaths.add(searchPath);
}
- public void addClasspath(Path classPath) {
+ public void addClassPath(Path classPath) {
Objects.requireNonNull(classPath);
searchPaths.add(new ClassPath(classPath));
}
- public void addClasspaths(String cps) {
- Arrays.stream(cps.split(File.pathSeparator))
- .filter(s -> !"".equals(s))
- .flatMap(s -> {
- if (s.endsWith(File.separatorChar + "*")) {
- try {
- return Files.list(Paths.get(s.substring(0, s.length() - 2)))
- .filter(p -> p.getFileName().toString().toLowerCase().endsWith(".jar"));
- } catch (IOException e) {
- return Stream.empty();
- }
- }
- return Stream.of(Paths.get(s));
- })
- .filter(Files::exists)
- .map(ClassPath::new)
- .forEachOrdered(searchPaths::add);
+ public void addModulePath(Path modulePath) {
+ Objects.requireNonNull(modulePath);
+ searchPaths.add(new ModulePath(modulePath));
}
- public void addRuntimeClasspath() {
- searchPaths.add(RuntimeClassPath.INSTANCE);
+ public Path getOutputDir() {
+ return outputDir;
}
- public List<SearchPath> searchPaths() {
- return searchPaths;
+ public void setOutputDir(Path outputDir) {
+ this.outputDir = outputDir;
}
public PrintWriter getErrorHandle() {
return errorHandle;
}
- public void setErrorHandle(Writer handle) {
- if (handle == null || handle instanceof PrintWriter) {
- errorHandle = (PrintWriter) handle;
+ public void setErrorHandle(Writer errorHandle) {
+ if (errorHandle instanceof PrintWriter || errorHandle == null) {
+ this.errorHandle = (PrintWriter) errorHandle;
} else {
- errorHandle = new PrintWriter(handle);
+ this.errorHandle = new PrintWriter(errorHandle);
}
}
-
- public boolean isOutputToSignalFile() {
- return outputToSignalFile;
- }
-
- public void setOutputToSignalFile(boolean outputToSignalFile) {
- this.outputToSignalFile = outputToSignalFile;
- }
-
- public Path getOutputPath() {
- return outputPath;
- }
-
- public void setOutputPath(Path outputPath) {
- this.outputPath = outputPath;
- }
-
- public void addClass(String name) {
- classList.add(name);
- }
-
- public List<String> getClassList() {
- return classList;
- }
}
diff --git a/src/main/java/org/glavo/javah/Main.java b/src/main/java/org/glavo/javah/Main.java
index 6df9539..1229520 100644
--- a/src/main/java/org/glavo/javah/Main.java
+++ b/src/main/java/org/glavo/javah/Main.java
@@ -1,71 +1,103 @@
package org.glavo.javah;
+import picocli.CommandLine;
+
+import java.io.File;
import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
import java.util.jar.Manifest;
+import java.util.stream.Stream;
+
+import static picocli.CommandLine.*;
+@Command(name = "gjavah", version = "gjavah %1", sortOptions = false)
public class Main {
- private static final String HELP_MESSAGE =
- "usage:\n" +
- " gjavah [options] <classes>\n" +
- "where [options] include:\n" +
- " -o <file> Output file (only one of -d or -o may be used)\n" +
- " -d <dir> Output directory\n" +
- " --module-path <path> Path from which to search modules\n" +
- " --class-path <path> | " +
- "-classpath <path> | -cp <path>\n" +
- " " +
- " Path from which to search classes\n" +
- " --version Print version information\n" +
- " -h --help -? Print this message";
+ @Option(names = {"-p", "--module-path"}, description = "Path from which to search modules")
+ private String modulePath;
+ @Option(names = {"-cp", "-classpath", "--classpath", "--class-path"}, description = "Path from which to search classes")
+ private String classpath;
+
+ @Option(names = {"-version", "--version"}, description = "Print version information")
+ private boolean showVersion;
+
+ @Option(names = {"-h", "--help", "-?"}, usageHelp = true, description = "Print this message")
+ private boolean showHelp;
+
+ @Option(names = {"-d"}, description = "Output directory")
+ private Path outputDir = Paths.get("").toAbsolutePath().normalize();
+
+ @Parameters(paramLabel = "classes")
+ private List<String> classes;
public static void main(String[] args) throws Exception {
- if (args.length == 0) {
- System.err.println(HELP_MESSAGE);
+ Main m = new Main();
+ CommandLine cm = new CommandLine(m);
+ if (args == null || args.length == 0) {
+ cm.usage(System.err);
System.exit(-1);
}
- JavahTask task = new JavahTask();
- task.addRuntimeClasspath();
- loop:
- for (int i = 0; i < args.length; i++) {
- String arg = args[i];
- switch (arg) {
- case "--version":
- case "-version":
- try (InputStream in = Main.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
- Manifest manifest = new Manifest(in);
- System.out.println("gjavah version: " + manifest.getMainAttributes().getValue("GJavah-Version"));
- }
- return;
- case "-h":
- case "--help":
- case "-help":
- case "-?":
- System.out.println(HELP_MESSAGE);
- return;
- case "-o":
- task.setOutputToSignalFile(true);
- task.setOutputPath(Paths.get(args[++i]));
- break;
- case "-d":
- task.setOutputToSignalFile(false);
- task.setOutputPath(Paths.get(args[++i]));
- break;
- case "--module-path":
- task.addModulePaths(args[++i]);
- break;
- case "--class-path":
- case "--classpath":
- case "-classpath":
- case "-cp":
- task.addClasspaths(args[++i]);
- break;
- default:
- while (i < args.length) {
- task.addClass(args[i++]);
- }
+
+ cm.parseArgs(args);
+ System.out.println(m);
+ if (m.showHelp) {
+ cm.usage(System.out);
+ return;
+ }
+ if (m.showVersion) {
+ try (InputStream in = Main.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
+ cm.printVersionHelp(System.out, Help.Ansi.AUTO, new Manifest(in).getMainAttributes().getValue("GJavah-Version"));
}
+ return;
}
+ if (m.classes == null || m.classes.isEmpty()) {
+ cm.usage(System.err);
+ System.exit(-1);
+ }
+
+ JavahTask task = new JavahTask();
+ if (m.modulePath != null) {
+ Arrays.stream(m.modulePath.split(File.pathSeparator))
+ .map(Paths::get)
+ .filter(Files::isDirectory)
+ .forEachOrdered(task::addModulePath);
+ }
+ if (m.classpath == null) {
+ m.classpath = System.getenv("CLASSPATH");
+ }
+ if (m.classpath == null) {
+ m.classpath = Paths.get("").toAbsolutePath().normalize().toString();
+ }
+ Arrays.stream(m.classpath.split(File.pathSeparator))
+ .flatMap(p -> {
+ if (p.endsWith("/*") || p.equals("*")) {
+ try {
+ return Files.list(Paths.get(p.substring(0, p.length() - 1)))
+ .filter(Files::isRegularFile)
+ .filter(t -> t.toAbsolutePath().getFileName().toString().toLowerCase().endsWith(".jar"));
+ } catch (Exception e) {
+ return Stream.empty();
+ }
+ }
+ return Stream.of(Paths.get(p));
+ })
+ .filter(Files::exists)
+ .map(Path::toAbsolutePath)
+ .forEachOrdered(task::addClassPath);
+ task.setOutputDir(m.outputDir);
+ task.addClasses(m.classes);
+ task.setErrorHandle(new PrintWriter(System.err, true));
+ task.addRuntimeSearchPath();
task.run();
}
+
+ @Override
+ public String toString() {
+ return String.format("Main[modulePath='%s', classpath='%s', showVersion=%s, showHelp=%s, outputDir=%s, classes=%s]", modulePath, classpath, showVersion, showHelp, outputDir, classes);
+ }
}
diff --git a/src/main/java/org/glavo/javah/ModulePath.java b/src/main/java/org/glavo/javah/ModulePath.java
index b355b24..0a9c2cc 100644
--- a/src/main/java/org/glavo/javah/ModulePath.java
+++ b/src/main/java/org/glavo/javah/ModulePath.java
@@ -1,34 +1,46 @@
package org.glavo.javah;
import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.*;
+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 final List<Path> paths;
+ private List<Path> roots;
public ModulePath(Path path) {
Objects.requireNonNull(path);
-
+ path = path.toAbsolutePath();
this.path = path;
- try {
- paths = Files.list(path)
- .filter(Files::isRegularFile)
- .flatMap(p -> Utils.getPathsFrom(p).stream())
- .collect(Collectors.toList());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ 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 searchClass(String className) {
- return Utils.searchFrom(paths, className);
+ public Path search(ClassName name) {
+ Objects.requireNonNull(name);
+ return SearchPath.searchFromRoots(roots, name);
}
@Override
diff --git a/src/main/java/org/glavo/javah/NativeMethod.java b/src/main/java/org/glavo/javah/NativeMethod.java
new file mode 100644
index 0000000..1500697
--- /dev/null
+++ b/src/main/java/org/glavo/javah/NativeMethod.java
@@ -0,0 +1,92 @@
+package org.glavo.javah;
+
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+
+import static org.glavo.javah.Utils.*;
+
+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 (!METHOD_NAME_PATTERN.matcher(name).matches()) {
+ throw new IllegalArgumentException(String.format("\"%s\" is not a qualified method name", name));
+ }
+ Matcher m = 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 = mangleName(name);
+ this.longMangledName = mangledName + "__" + 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/src/main/java/org/glavo/javah/RuntimeClassPath.java b/src/main/java/org/glavo/javah/RuntimeClassPath.java
deleted file mode 100644
index c16cfa3..0000000
--- a/src/main/java/org/glavo/javah/RuntimeClassPath.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.glavo.javah;
-
-import java.net.URI;
-import java.net.URL;
-import java.nio.file.*;
-
-public class RuntimeClassPath implements SearchPath {
- public static final RuntimeClassPath INSTANCE = new RuntimeClassPath();
-
- private RuntimeClassPath() {
- }
-
- @Override
- @SuppressWarnings("ConstantConditions")
- public Path searchClass(String className) {
- className = Utils.fullClassNameOf(className);
- URI uri = null;
- try {
- Class<?> cls = Class.forName(className);
- URL url = cls.getResource(Utils.simpleNameOf(className) + ".class");
- if (url == null) {
- return null;
- }
- uri = url.toURI();
- return Paths.get(uri);
- } catch (FileSystemNotFoundException e) {
- try {
- FileSystem fs = FileSystems.newFileSystem(uri, null);
- Path p = fs.getPath(className.replace('.', '/') + ".class");
- if (Files.isRegularFile(p)) {
- return p;
- }
- return null;
- } catch (Exception ex) {
- return null;
- }
- } catch (Exception e) {
- return null;
- }
- }
-}
diff --git a/src/main/java/org/glavo/javah/RuntimeSearchPath.java b/src/main/java/org/glavo/javah/RuntimeSearchPath.java
new file mode 100644
index 0000000..fdfe717
--- /dev/null
+++ b/src/main/java/org/glavo/javah/RuntimeSearchPath.java
@@ -0,0 +1,44 @@
+package org.glavo.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/src/main/java/org/glavo/javah/SearchPath.java b/src/main/java/org/glavo/javah/SearchPath.java
index b95b7c0..adbe27d 100644
--- a/src/main/java/org/glavo/javah/SearchPath.java
+++ b/src/main/java/org/glavo/javah/SearchPath.java
@@ -1,7 +1,92 @@
package org.glavo.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;
+
+import static org.glavo.javah.Utils.*;
public interface SearchPath {
- Path searchClass(String className) ;
+ Path search(ClassName name);
+
+ default Path search(String fullName) {
+ Objects.requireNonNull(fullName);
+ return search(ClassName.of(fullName));
+ }
+
+ static Path searchFrom(Iterable<SearchPath> 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<Path> 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<Path> 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<Path> list = Files.list(base)
+ .map(Path::toAbsolutePath)
+ .filter(Files::isDirectory)
+ .filter(p -> 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/src/main/java/org/glavo/javah/Utils.java b/src/main/java/org/glavo/javah/Utils.java
index e9043ea..cb1ceef 100644
--- a/src/main/java/org/glavo/javah/Utils.java
+++ b/src/main/java/org/glavo/javah/Utils.java
@@ -1,221 +1,134 @@
package org.glavo.javah;
-import org.objectweb.asm.Type;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
import java.io.IOException;
-import java.io.InputStream;
+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.*;
-import java.util.jar.Manifest;
+import java.util.Collections;
+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 {
- static String encode(String str) {
- StringBuilder buffer = new StringBuilder(str.length());
+ public static final int MAX_SUPPORTED_VERSION = 13;
- int len = str.length();
- for (int i = 0; i < len; i++) {
- char ch = str.charAt(i);
- if (ch == '_') {
- buffer.append("_1");
- } else if (ch == ';') {
- buffer.append("_2");
- } else if (ch == '[') {
- buffer.append("_3");
- } else if ((ch >= '0' && ch <= '9')
- || (ch >= 'a' && ch <= 'z')
- || (ch >= 'A' && ch <= 'Z')
- || ch == '/'
- || ch == '.') {
- buffer.append(ch);
- } else {
- buffer.append(String.format("_0%04x", (int) ch));
- }
- }
- return buffer.toString();
- }
+ public static final List<String> MULTI_RELEASE_VERSIONS =
+ IntStream.rangeClosed(9, MAX_SUPPORTED_VERSION).mapToObj(Integer::toString).collect(Collectors.toList());
- static String mapToNativeType(Type jt) {
- switch (jt.toString()) {
- 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";
- }
+ public static final Pattern SIMPLE_NAME_PATTERN = Pattern.compile("[^.;\\[/]+");
+ public static final Pattern FULL_NAME_PATTERN =
+ Pattern.compile("[^.;\\[/]+(\\.[^.;\\[/]+)*");
- if (jt.toString().startsWith("[")) {
- return "jobjectArray";
- }
-
- return "jobject";
- }
+ public static final Pattern METHOD_NAME_PATTERN = Pattern.compile("(<init>)|(<cinit>)|([^.;\\[/<>]+)");
+ public static final Pattern METHOD_TYPE_PATTERN =
+ Pattern.compile("\\((?<args>(\\[*([BCDFIJSZ]|L[^.;\\[/]+(/[^.;\\\\\\[/]+)*;))*)\\)(?<ret>\\[*([BCDFIJSZV]|L[^.;\\[/]+(/[^.;\\[/]+)*;))");
- static String mangledArgSignature(Type methodType) {
- Objects.requireNonNull(methodType);
- String str = methodType.toString();
- int idx = str.indexOf(')');
- if (idx == -1) {
- throw new IllegalArgumentException(methodType.toString() + "is not a method type");
+ public static final PrintWriter NOOP_WRITER = new PrintWriter(new Writer() {
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
}
- return encode(str.substring(1, idx));
- }
-
- static boolean isRegularClassName(String name) {
- Objects.requireNonNull(name);
- return !name.contains("//")
- && !name.contains("..")
- && name.indexOf('\\') == -1
- && name.indexOf(';') == -1
- && name.indexOf('[') == -1;
- }
- static String fullClassNameOf(String name) {
- Objects.requireNonNull(name);
- int idx = name.indexOf('/');
- if (idx == -1) {
- return name;
+ @Override
+ public void flush() throws IOException {
}
- String cn = name.substring(idx + 1);
- if (cn.isEmpty() || cn.indexOf('/') == -1) {
- throw new IllegalArgumentException("Illegal class name: " + name);
- }
- return cn;
- }
- static String simpleNameOf(String name) {
- int idx = name.lastIndexOf('.');
- return name.substring(idx + 1);
- }
+ @Override
+ public void close() throws IOException {
+ }
+ });
- static Path searchFromPathList(List<Path> paths, String className) {
- Objects.requireNonNull(paths);
- Objects.requireNonNull(className);
- String fp = fullClassNameOf(className).replace('.', '/') + ".class";
- for (Path path : paths) {
- Objects.requireNonNull(path);
- Path p = path.resolve(fp);
- if (Files.isRegularFile(p)) {
- return p;
+ 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 null;
+ return builder.toString();
}
- static List<Path> getPathsFrom(Path root) {
- Objects.requireNonNull(root);
- root = root.toAbsolutePath();
- if (Files.isRegularFile(root)) {
- String name = root.toString().toLowerCase();
- try {
- FileSystem fs = FileSystems.newFileSystem(root, (ClassLoader) null);
- if (name.endsWith(".jar")) {
- root = fs.getPath("/");
- } else if (name.endsWith(".jmod")) {
- root = fs.getPath("/classes");
- } else {
- return Collections.emptyList();
- }
- } catch (IOException e) {
- return Collections.emptyList();
+ 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));
}
}
- if (!Files.isDirectory(root)) {
- return Collections.emptyList();
- }
+ return builder.toString();
+ }
- boolean isMultiRelease = false;
- Path manifest = root.resolve("META-INF").resolve("MANIFEST.MF");
- if (Files.exists(manifest) && Files.isRegularFile(manifest)) {
- try (InputStream input = Files.newInputStream(manifest)) {
- isMultiRelease = Boolean.parseBoolean(
- new Manifest(input).getMainAttributes().getOrDefault("Multi-Release", "false").toString()
- );
+ public static Path classPathRoot(Path p) {
+ Objects.requireNonNull(p);
+ p = p.toAbsolutePath();
- } catch (Exception ignored) {
- }
+ if (Files.notExists(p)) {
+ return null;
}
-
- if (!isMultiRelease) {
- return Collections.singletonList(root);
+ if (Files.isDirectory(p)) {
+ return p;
}
- LinkedList<Path> list = new LinkedList<>();
- Path mr = root.resolve("META-INF").resolve("versions");
+
try {
- Files.list(mr)
- .filter(Utils::checkMultiReleaseVersion)
- .sorted(Comparator.comparingInt(a -> Integer.parseInt(a.getFileName().toString())))
- .forEachOrdered(list::addFirst);
+ 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;
}
-
- list.add(root);
- return list;
+ return null;
}
- private static final String[] MULTI_VERSIONS = {"9", "10", "11", "12", "13"};
-
- private static boolean checkMultiReleaseVersion(Path p) {
- Objects.requireNonNull(p);
- String n = p.getFileName().toString();
- for (String version : MULTI_VERSIONS) {
- if (version.equals(n)) {
- return true;
+ public static ClassName superClassOf(ClassReader reader) {
+ Objects.requireNonNull(reader);
+ class V extends ClassVisitor {
+ V() {
+ super(Opcodes.ASM7);
}
- }
- return false;
- }
-
- static Path searchFrom(List<Path> searchPaths, String name) {
- Objects.requireNonNull(searchPaths);
- Objects.requireNonNull(name);
- name = simpleNameOf(name).replace('.', '/') + ".class";
+ ClassName superName = null;
- for (Path searchPath : searchPaths) {
- Path p = searchPath.resolve(name);
- if (Files.isRegularFile(p)) {
- return p;
+ @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('/', '.'));
+ }
}
}
- return null;
+ V v = new V();
+ reader.accept(v, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
+ return v.superName;
}
}
diff --git a/src/test/java/org/glavo/javah/ClassMetaInfoTest.java b/src/test/java/org/glavo/javah/ClassMetaInfoTest.java
new file mode 100644
index 0000000..9c1042c
--- /dev/null
+++ b/src/test/java/org/glavo/javah/ClassMetaInfoTest.java
@@ -0,0 +1,54 @@
+package org.glavo.javah;
+
+import org.junit.jupiter.api.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ClassMetaInfoTest {
+ static class TestData {
+ ClassName name;
+ ClassName superName;
+
+ TestData(ClassName name, ClassName superName) {
+ this.name = name;
+ this.superName = superName;
+ }
+ }
+
+ class C1 {
+ public native int f();
+ }
+
+ @Test
+ void test() throws IOException, ClassNotFoundException {
+ TestData[] data = new TestData[]{
+ new TestData(ClassName.of("java.lang.Object"), null),
+ new TestData(ClassName.of("java.lang.String"), ClassName.of("java.lang.Object")),
+ new TestData(ClassName.of(C1.class.getName()), ClassName.of(C1.class.getSuperclass().getName()))
+ };
+ for (TestData d : data) {
+ Class<?> cls = Class.forName(d.name.className());
+ List<NativeMethod> methods = Arrays.stream(cls.getDeclaredMethods())
+ .filter(m -> (m.getModifiers() & Modifier.NATIVE) != 0)
+ .map(m -> NativeMethod.of(m.getModifiers(), m.getName(), Type.getMethodDescriptor(m)))
+ .collect(Collectors.toList());
+
+ ClassMetaInfo info = new ClassMetaInfo();
+ ClassReader reader = new ClassReader(d.name.className());
+ reader.accept(info, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
+
+ assertEquals(info.name, d.name);
+ assertEquals(info.superClassName, d.superName);
+
+ assertTrue(info.methods.containsAll(methods) && methods.containsAll(info.methods));
+ }
+ }
+}
diff --git a/src/test/java/org/glavo/javah/NativeMethodTests.java b/src/test/java/org/glavo/javah/NativeMethodTests.java
new file mode 100644
index 0000000..32251f9
--- /dev/null
+++ b/src/test/java/org/glavo/javah/NativeMethodTests.java
@@ -0,0 +1,34 @@
+package org.glavo.javah;
+
+import org.junit.jupiter.api.Test;
+import org.objectweb.asm.Type;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class NativeMethodTests {
+ @Test
+ void testFactoryMethod() {
+ Map<String, Type> qualified = new LinkedHashMap<>() {
+ {
+ put("method0", Type.getType("()I"));
+ put("method1", Type.getType("(Ljava/lang/String;)I"));
+ }
+ };
+
+ Map<String, Type> wrongs = new LinkedHashMap<>() {
+ {
+ put("method0", Type.getType(String.class));
+ put("method1", Type.getType("()"));
+ put("method2", Type.getType("L;"));
+ }
+ };
+
+ qualified.forEach((name, type) -> assertDoesNotThrow(() -> NativeMethod.of(name, type)));
+ wrongs.forEach((name, type) -> assertThrows(IllegalArgumentException.class, () -> NativeMethod.of(name, type)));
+
+
+ }
+}
diff --git a/src/test/java/org/glavo/javah/RegexTests.java b/src/test/java/org/glavo/javah/RegexTests.java
new file mode 100644
index 0000000..026c5a5
--- /dev/null
+++ b/src/test/java/org/glavo/javah/RegexTests.java
@@ -0,0 +1,73 @@
+package org.glavo.javah;
+
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RegexTests {
+
+ @Test
+ void testSimpleNamePattern() {
+ String[] names = {
+ "A", "a", "ABC", "AbC", "类名称", "_(*)", "a b c $ d ,", "<a"
+ };
+
+ String[] wrongNames = {
+ "", "A.B", "[A", "A;B", "A/B"
+ };
+
+ for (String name : names) {
+ assertTrue(Utils.SIMPLE_NAME_PATTERN.matcher(name).matches()
+ , String.format("'%s' match simple name pattern failed", name));
+ }
+
+ for (String name : wrongNames) {
+ assertFalse(Utils.SIMPLE_NAME_PATTERN.matcher(name).matches(),
+ String.format("'%s' match simple name pattern failed", name));
+ }
+ }
+
+ @Test
+ void testFullNamePattern() {
+ String[] names = {
+ "A", "a", "ABC", "AbC", "类名称", "_(*)", "a b c $ d ,",
+ "A.B.C", "A.bcd.E", "包1.包2.类名称", "_().B"
+ };
+
+ String[] wrongNames = {
+ "", "A..B", "A.", ".A", "[A", "A;B", "A/B"
+ };
+
+ for (String name : names) {
+ assertTrue(Utils.FULL_NAME_PATTERN.matcher(name).matches(),
+ String.format("'%s' match full name pattern failed", name));
+ }
+
+ for (String name : wrongNames) {
+ assertFalse(Utils.FULL_NAME_PATTERN.matcher(name).matches(),
+ String.format("'%s' match full name pattern failed", name));
+ }
+ }
+
+ @Test
+ void testMethodNamePattern() {
+ String[] names = {
+ "A", "a", "ABC", "AbC", "类名称", "_(*)", "a b c $ d ,", "<init>", "<cinit>"
+ };
+
+ String[] wrongNames = {
+ "", "A.B", "[A", "A;B", "A/B", "<", "b<a"
+ };
+
+ for (String name : names) {
+ assertTrue(Utils.METHOD_NAME_PATTERN.matcher(name).matches()
+ , String.format("'%s' match simple name pattern failed", name));
+ }
+
+ for (String name : wrongNames) {
+ assertFalse(Utils.METHOD_NAME_PATTERN.matcher(name).matches(),
+ String.format("'%s' match simple name pattern failed", name));
+ }
+ }
+}
diff --git a/src/test/java/org/glavo/javah/RuntimeSearchPathTests.java b/src/test/java/org/glavo/javah/RuntimeSearchPathTests.java
new file mode 100644
index 0000000..2530312
--- /dev/null
+++ b/src/test/java/org/glavo/javah/RuntimeSearchPathTests.java
@@ -0,0 +1,31 @@
+package org.glavo.javah;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RuntimeSearchPathTests {
+
+ @Test
+ void test() throws Exception {
+ Class<?>[] testClasses = {
+ String.class,
+ Test.class,
+ RuntimeSearchPathTests.class,
+ Main.class
+ };
+
+ for (Class<?> cls : testClasses) {
+ try (InputStream in = cls.getResourceAsStream(cls.getSimpleName() + ".class")) {
+ assertArrayEquals(
+ Files.readAllBytes(RuntimeSearchPath.searchClass(cls.getName())),
+ in.readAllBytes(),
+ "Search " + cls + " failed"
+ );
+ }
+ }
+ }
+}