aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md12
-rw-r--r--build.gradle68
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin54329 -> 58702 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--[-rwxr-xr-x]gradlew51
-rw-r--r--gradlew.bat184
-rw-r--r--src/main/java/module-info.java8
-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.java45
-rw-r--r--src/main/java/org/glavo/javah/Constant.java83
-rw-r--r--src/main/java/org/glavo/javah/HeaderGenerator.java529
-rw-r--r--src/main/java/org/glavo/javah/JNIGenerator.java238
-rw-r--r--src/main/java/org/glavo/javah/JavahTask.java82
-rw-r--r--src/main/java/org/glavo/javah/Main.java103
-rw-r--r--src/main/java/org/glavo/javah/ModulePath.java50
-rw-r--r--src/main/java/org/glavo/javah/NativeMethod.java92
-rw-r--r--src/main/java/org/glavo/javah/RuntimeSearchPath.java44
-rw-r--r--src/main/java/org/glavo/javah/SearchPath.java92
-rw-r--r--src/main/java/org/glavo/javah/Utils.java134
-rw-r--r--src/test/java/org/glavo/javah/ClassMetaInfoTest.java54
-rw-r--r--src/test/java/org/glavo/javah/HeaderGeneratorTest.java12
-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
-rw-r--r--src/test/java/org/glavo/javah/Test.java9
26 files changed, 1482 insertions, 682 deletions
diff --git a/README.md b/README.md
index 42f596a..0600b90 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ repositories {
}
dependencies {
- compile 'org.glavo.glavo:gjavah:0.1.1'
+ compile 'org.glavo.glavo:gjavah:0.2.0'
}
```
@@ -16,8 +16,10 @@ dependencies {
```java
import org.glavo.javah.*;
-var writer = new PrintWriter(System.out);
-HeaderGenerator.generateHeader("java.lang.Object", writer);
-writer.flush();
-
+var task = new JavahTask();
+task.setOutputDir(Paths.get(""));
+task.addRuntimeSearchPath();
+task.addClass("java.lang.Object");
+task.addClass("java.lang.String");
+task.run();
``` \ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 1a66499..600a966 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,35 +1,59 @@
+plugins {
+ id 'java'
+ id "org.javamodularity.moduleplugin" version "1.6.0"
+}
+
group 'org.glavo'
-version '0.1.1'
+version '0.2'
+
+modularity.mixedJavaRelease 8
-apply plugin: 'java'
+jar {
+ manifest.attributes(
+ 'Implementation-Version': '1.2',
+ 'Main-Class': 'org.glavo.javah.Main',
+ "GJavah-Version": project.version
+ )
+}
-sourceCompatibility = 1.8
-targetCompatibility = 1.8
+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 {
- compile group: 'org.ow2.asm', name: 'asm', version: '6.1.1'
+ // https://mvnrepository.com/artifact/commons-cli/commons-cli
+ implementation group: 'info.picocli', name: 'picocli', version: '4.1.1'
- testCompile group: 'junit', name: 'junit', version: '4.12'
-}
+ // https://mvnrepository.com/artifact/org.ow2.asm/asm
+ implementation group: 'org.ow2.asm', name: 'asm', version: '7.2'
-jar {
- manifest {
- attributes 'Implementation-Version': '1.2',
- 'Main-Class': 'org.glavo.javah.HeaderGenerator'
- }
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.5.2'
}
-task fatJar(type: Jar) {
- baseName = project.name + '-all'
- description = 'Assembles a jar archive containing the main classes and all the dependencies.'
- from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
- manifest {
- attributes 'Implementation-Version': '1.2',
- 'Main-Class': 'org.glavo.javah.HeaderGenerator'
- }
- with jar
-} \ No newline at end of file
+task copyDependencies(type: Copy) {
+ from configurations.default
+ into 'build/libs'
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 01b8bf6..cc4fdc2 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 933b647..9492014 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip
diff --git a/gradlew b/gradlew
index cccdd3d..2fe81a7 100755..100644
--- a/gradlew
+++ b/gradlew
@@ -1,5 +1,21 @@
#!/usr/bin/env sh
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@@ -138,19 +154,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
-fi
-
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index e95643d..9618d8d 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,84 +1,100 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..bac1bc3
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +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
new file mode 100644
index 0000000..3e59c59
--- /dev/null
+++ b/src/main/java/org/glavo/javah/ClassPath.java
@@ -0,0 +1,45 @@
+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> roots;
+
+ public ClassPath(Path path) {
+ Objects.requireNonNull(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 int hashCode() {
+ return path.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "ClassPath[" + path + "]";
+ }
+}
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/HeaderGenerator.java b/src/main/java/org/glavo/javah/HeaderGenerator.java
deleted file mode 100644
index 81fe6c5..0000000
--- a/src/main/java/org/glavo/javah/HeaderGenerator.java
+++ /dev/null
@@ -1,529 +0,0 @@
-package org.glavo.javah;
-
-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<String> THROWABLE_NAME_LIST = Arrays.asList("Ljava/lang/Throwable;", "Ljava/lang/Error;", "Ljava/lang/Exception");
- private static final HeaderGenerator generator = new HeaderGenerator();
- public static final String VERSION = "0.1.1";
- private static final String message = "Usage: \n" +
- " javah [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" +
- " -h -help --help -? Print this message\n" +
- " -version Print version information\n" +
- " -classpath <path> Path from which to load classes\n" +
- " -cp <path> Path from which to load classes\n" +
- "<classes> are specified with their fully qualified names\n" +
- "(for example, java.lang.Object).";
-
- private static String escape(String source) {
- 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 '/':
- 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());
-
- for (Map.Entry<String, Set<MethodDesc>> entry : generator.getMethods().entrySet()) {
- boolean overload = entry.getValue().size() > 1;
- for (MethodDesc desc : entry.getValue()) {
- String methodName = escape(entry.getKey());
- 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_" + className + "_" + methodName
- );
-
- if (overload) {
- output.print("__");
- for (Type tpe : argTypes) {
- output.print(escape(tpe.toString()));
- }
- }
- 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) {
- StringBuilder builder = new StringBuilder();
- String className = escape(generator.getClassName());
-
- 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 <jni.h>");
-
- 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 main(String[] args) throws IOException {
- boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("windows");
- ArrayList<Path> cps = new ArrayList<>();
- Path outputFile = null;
- Path outputDir = null;
- ArrayList<String> classNames = new ArrayList<>();
-
- if (args.length == 0) {
- System.out.println(message);
- return;
- }
- for (int i = 0; i < args.length; i++) {
- String arg = args[i];
- if (arg.startsWith("-")) {
- switch (arg) {
- case "-h":
- case "-help":
- case "--help":
- case "-?":
- System.out.println(message);
- return;
- case "-version":
- System.out.println();
- return;
- case "-cp":
- case "-classpath":
- i++;
- if (i == args.length) {
- System.err.println("javah: " + arg + " requires an argument.");
- return;
- }
- String[] s = args[i].split(isWindows ? ";" : ":");
- for (String ss : s) {
- Path p = Paths.get(ss);
- if (Files.exists(p)) {
- if (Files.isDirectory(p)) {
- cps.add(p);
- } else if (ss.toLowerCase().endsWith(".jar") || ss.toLowerCase().endsWith(".zip")) {
- try {
- cps.add(FileSystems.newFileSystem(p, null).getPath("/"));
- } catch (Exception ignored) {
- }
- }
- }
- }
- break;
- case "-o":
- i++;
- if (i == args.length) {
- System.err.println("javah: " + arg + " requires an argument.");
- return;
- }
- if (outputDir != null) {
- System.err.println("Error: Can't mix options -d and -o. Try -help.");
- return;
- }
- outputFile = Paths.get(args[i]);
- break;
-
- case "-d":
- i++;
- if (i == args.length) {
- System.err.println("javah: " + arg + " requires an argument.");
- return;
- }
- if (outputFile != null) {
- System.err.println("Error: Can't mix options -d and -o. Try -help.");
- return;
- }
- outputDir = Paths.get(args[i]);
-
- break;
- default:
- System.err.println("Error: unknown option: " + arg);
- return;
- }
-
- } else {
- if (arg.contains("/")) {
- int idx = arg.indexOf('/');
- if (idx != arg.lastIndexOf('/')) {
- System.err.println("Not a valid class name: " + arg);
- }
- arg = arg.substring(idx + 1);
- }
- classNames.add(arg);
- }
-
- if (outputDir == null && outputFile == null) {
- outputDir = Paths.get(System.getProperty("user.dir", "."));
- }
- if (cps.isEmpty()) {
- cps.add(Paths.get(System.getProperty("user.dir", ".")));
- }
-
- if (outputDir != null && Files.notExists(outputDir)) {
- Files.createDirectories(outputDir);
- }
-
- if (outputFile != null && Files.notExists(outputFile)) {
- if (Files.notExists(outputFile.getParent())) {
- Files.createDirectories(outputFile.getParent());
- }
- }
-
- HeaderGenerator generator = new HeaderGenerator(cps.toArray(new Path[0]));
- if (outputFile != null) {
- if (Files.notExists(outputFile.getParent())) {
- Files.createDirectories(outputFile.getParent());
- }
- try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
- boolean first = true;
- 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);
- if (first) {
- generator.classGenerateHeader(g, writer);
- first = false;
- } else {
- generator.classGenerateHeaderWithoutInclude(g, writer);
- }
- }
- }
- }
- if (outputDir != null) {
- 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('.', '_') + ".h"), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
- generator.classGenerateHeader(g, writer);
- }
- }
- }
-
- }
- }
-}
-
-class MethodDesc {
- public final boolean isStatic;
- public final String descriptor;
-
- public MethodDesc(boolean isStatic, String descriptor) {
- this.isStatic = isStatic;
- this.descriptor = descriptor;
- }
-}
-
-class Generator extends ClassVisitor {
- private String className;
-
- private Map<String, String> constants = new LinkedHashMap<>();
- private Map<String, Set<MethodDesc>> methods = new LinkedHashMap<String, Set<MethodDesc>>();
-
- 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<MethodDesc> 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<String, Set<MethodDesc>> getMethods() {
- return methods;
- }
-} \ No newline at end of file
diff --git a/src/main/java/org/glavo/javah/JNIGenerator.java b/src/main/java/org/glavo/javah/JNIGenerator.java
new file mode 100644
index 0000000..bec2ea9
--- /dev/null
+++ b/src/main/java/org/glavo/javah/JNIGenerator.java
@@ -0,0 +1,238 @@
+package org.glavo.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.*;
+
+import static org.glavo.javah.Utils.*;
+
+public class JNIGenerator {
+
+ private final PrintWriter errorHandle;
+ private final Iterable<SearchPath> searchPaths;
+ private final Path outputDir;
+
+ public JNIGenerator(Path outputDir) {
+ this(outputDir, null, null);
+ }
+
+ public JNIGenerator(Path outputDir, Iterable<SearchPath> searchPaths) {
+ this(outputDir, searchPaths, null);
+ }
+
+ public JNIGenerator(Path outputDir, Iterable<SearchPath> searchPaths, PrintWriter errorHandle) {
+ Objects.requireNonNull(outputDir);
+
+ 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(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 <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());
+ }
+
+ 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();
+ }
+
+ 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 (!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(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
new file mode 100644
index 0000000..8e239ed
--- /dev/null
+++ b/src/main/java/org/glavo/javah/JavahTask.java
@@ -0,0 +1,82 @@
+package org.glavo.javah;
+
+import java.io.OutputStreamWriter;
+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<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);
+ }
+ }
+ }
+
+ 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<String> 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/src/main/java/org/glavo/javah/Main.java b/src/main/java/org/glavo/javah/Main.java
new file mode 100644
index 0000000..3c2be04
--- /dev/null
+++ b/src/main/java/org/glavo/javah/Main.java
@@ -0,0 +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 ${gjavah.version}", sortOptions = false)
+public class Main {
+ @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 {
+ Main m = new Main();
+ CommandLine cm = new CommandLine(m);
+ if (args == null || args.length == 0) {
+ cm.usage(System.err);
+ System.exit(-1);
+ }
+
+ cm.parseArgs(args);
+ if (m.showHelp) {
+ cm.usage(System.out);
+ return;
+ }
+ if (m.showVersion) {
+ try (InputStream in = Main.class.getResourceAsStream("/META-INF/MANIFEST.MF")) {
+ System.setProperty("gjavah.version", new Manifest(in).getMainAttributes().getValue("GJavah-Version"));
+ cm.printVersionHelp(System.out);
+ }
+ 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
new file mode 100644
index 0000000..0a9c2cc
--- /dev/null
+++ b/src/main/java/org/glavo/javah/ModulePath.java
@@ -0,0 +1,50 @@
+package org.glavo.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<Path> 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/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/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
new file mode 100644
index 0000000..adbe27d
--- /dev/null
+++ b/src/main/java/org/glavo/javah/SearchPath.java
@@ -0,0 +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 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
new file mode 100644
index 0000000..cb1ceef
--- /dev/null
+++ b/src/main/java/org/glavo/javah/Utils.java
@@ -0,0 +1,134 @@
+package org.glavo.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.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 {
+ public static final int MAX_SUPPORTED_VERSION = 13;
+
+ public static final List<String> 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("(<init>)|(<cinit>)|([^.;\\[/<>]+)");
+ public static final Pattern METHOD_TYPE_PATTERN =
+ Pattern.compile("\\((?<args>(\\[*([BCDFIJSZ]|L[^.;\\[/]+(/[^.;\\\\\\[/]+)*;))*)\\)(?<ret>\\[*([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.ASM7);
+ }
+
+ 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/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/HeaderGeneratorTest.java b/src/test/java/org/glavo/javah/HeaderGeneratorTest.java
deleted file mode 100644
index b3f0380..0000000
--- a/src/test/java/org/glavo/javah/HeaderGeneratorTest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.glavo.javah;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-
-public class HeaderGeneratorTest {
- public static void main(String[] args) throws IOException {
- PrintWriter writer = new PrintWriter(System.out);
- HeaderGenerator.generateHeader("java.lang.System", writer);
- writer.flush();
- }
-}
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"
+ );
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/glavo/javah/Test.java b/src/test/java/org/glavo/javah/Test.java
deleted file mode 100644
index 989b37e..0000000
--- a/src/test/java/org/glavo/javah/Test.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.glavo.javah;
-
-public class Test {
- public native String[][] f1();
-
- public native Throwable f2();
-
- public native Class<?>[] f3();
-}