diff options
26 files changed, 1482 insertions, 682 deletions
@@ -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 Binary files differindex 01b8bf6..cc4fdc2 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar 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 @@ -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(); -} |