diff --git a/jni.d b/jni.d index 7f6ff43..bd20c5f 100644 --- a/jni.d +++ b/jni.d @@ -120,15 +120,85 @@ +/ module arsd.jni; -// FIXME: java.lang.CharSequence is the interface for String. We should support that just as well. +// FIXME: if user defines an interface with the appropriate RAII return values, +// it should let them do that for more efficiency +// e.g. @Import Manual!(int[]) getJavaArray(); + +/+ +final class CharSequence : JavaClass!("java.lang", CharSequence) { + @Import string toString(); // this triggers a dmd segfault! whoa. FIXME dmd +} ++/ + +/++ + This is an interface in Java that the built in String class + implements. In D, our string is not an object at all, and + overloading an auto-generated method with a user-defined one + is a bit awkward and not automatic, so I'm instead cheating + here and pretending it is a class we can construct in a D + overload. + + This is not a great solution. I'm also considering a UDA + on the param to indicate Java actually expects the interface, + or even a runtime lookup to disambiguate, but meh for now + I'm just sticking with this slightly wasteful overload/construct. ++/ +final class CharSequence : JavaClass!("java.lang", CharSequence) { + this(string data) { + auto env = activeEnv; + assert(env !is null); + + wchar[1024] buffer; + const(wchar)[] translated; + if(data.length < 1024) { + size_t len; + foreach(wchar ch; data) + buffer[len++] = ch; + translated = buffer[0 .. len]; + } else { + import std.conv; + translated = to!wstring(data); + } + // Java copies the buffer so it is perfectly fine to return here now + internalJavaHandle_ = (*env).NewString(env, translated.ptr, cast(jsize) translated.length); + } + this(wstring data) { + auto env = activeEnv; + assert(env !is null); + internalJavaHandle_ = (*env).NewString(env, data.ptr, cast(jsize) data.length); + } +} + +/++ + Can be used as a UDA for methods or classes where the D name + and the Java name don't match (for example, if it happens to be + a D keyword). + + --- + @JavaName("version") + @Import int version_(); + --- ++/ +struct JavaName { + string name; +} + +private string getJavaName(alias a)() { + string name = __traits(identifier, a); + static foreach(attr; __traits(getAttributes, a)) + static if(is(typeof(attr) == JavaName)) + name = attr.name; + return name; +} + +// semi-FIXME: java.lang.CharSequence is the interface for String. We should support that just as well. +// possibly other boxed types too, like Integer. // FIXME: in general, handle substituting subclasses for interfaces nicely // FIXME: solve the globalref/pin issue with the type system // -// FIXME: in general i didn't handle overloads at all. Using RegisterNatives should make that doable... and I might be able to remove some of the pragma(mangle) that way too. And get more error checking done up-front. - // FIXME: what about the parent class of the java object? Best we can probably do is an interface but perhaps it could be auto-generated by the JavaClass magic. It could take the list and just copy the @Import items. /+ @@ -153,30 +223,8 @@ module arsd.jni; //pragma(crt_constructor) // fyi //pragma(crt_destructor) -// FIXME: put this in a mixin instead of assuming it is needed/wanted? - -extern(C) -jint JNI_OnLoad(JavaVM* vm, void* reserved) { -/+ - - JNIEnv* env; - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - - // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. - jclass c = env->FindClass("com/example/app/package/MyClass"); - if (c == nullptr) return JNI_ERR; - - // Register your class' native methods. - static const JNINativeMethod methods[] = { - {"nativeFoo", "()V", reinterpret_cast(nativeFoo)}, - {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast(nativeBar)}, - }; - int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); - if (rc != JNI_OK) return rc; - -+/ +extern(System) +export jint JNI_OnLoad(JavaVM* vm, void* reserved) { try { import core.runtime; // note this is OK if it is already initialized @@ -187,10 +235,20 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { } activeJvm = vm; + + JNIEnv* env; + if ((*vm).GetEnv(vm, cast(void**) &env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + foreach(init; classInitializers_) + if(init(env) != 0) + return JNI_ERR; + return JNI_VERSION_1_6; } -extern(C) -void JNI_OnUnload(JavaVM* vm, void* reserved) { +extern(System) +export void JNI_OnUnload(JavaVM* vm, void* reserved) { activeJvm = null; import core.runtime; try { @@ -224,17 +282,44 @@ struct ActivateJniEnv { /++ Creates a JVM for use when `main` is in D. Keep the returned - struct around until you are done with it. + struct around until you are done with it. While this struct + is live, you can use imported Java classes and methods almost + as if they were written in D. If `main` is in Java, this is not necessary and should not be used. - If you use this, you will need to link in a jni shared lib. + + This function will try to load the jvm with dynamic runtime + linking. For this to succeed: + + On Windows, make sure the path to `jvm.dll` is in your `PATH` + environment variable, or installed system wide. For example: + + $(CONSOLE + set PATH=%PATH%;c:\users\me\program\jdk-13.0.1\bin\server + ) + + On Linux (and I think Mac), set `LD_LIBRARY_PATH` environment + variable to include the path to `libjvm.so`. + + $(CONSOLE + export LD_LIBRARY_PATH=/home/me/jdk-13.0.1/bin/server + ) + + Failure to do this will throw an exception along the lines of + "no jvm dll" in the message. +/ auto createJvm()() { - struct JVM { + version(Windows) + import core.sys.windows.windows; + else + import core.sys.posix.dlfcn; + + static struct JVM { ActivateJniEnv e; JavaVM* pvm; + void* jvmdll; @disable this(this); @@ -242,22 +327,77 @@ auto createJvm()() { if(pvm) (*pvm).DestroyJavaVM(pvm); activeJvm = null; + + version(Windows) { + if(jvmdll) FreeLibrary(jvmdll); + } else { + if(jvmdll) dlclose(jvmdll); + } } } JavaVM* pvm; JNIEnv* env; - auto res = JNI_CreateJavaVM(&pvm, cast(void**) &env, null); + + JavaVMInitArgs vm_args; + JavaVMOption[0] options; + + //options[0].optionString = "-Djava.compiler=NONE"; /* disable JIT */ + //options[1].optionString = "-verbose:jni"; /* print JNI-related messages */ + //options[1].optionString = `-Djava.class.path=c:\Users\me\program\jni\`; /* user classes */ + //options[2].optionString = `-Djava.library.path=c:\Users\me\program\jdk-13.0.1\lib\`; /* set native library path */ + + vm_args.version_ = JNI_VERSION_1_6; + vm_args.options = options.ptr; + vm_args.nOptions = cast(int) options.length; + vm_args.ignoreUnrecognized = true; + + //import std.process; + //environment["PATH"] = environment["PATH"] ~ `;c:\users\me\program\jdk-13.0.1\bin\server`; + + version(Windows) + auto jvmdll = LoadLibraryW("jvm.dll"w.ptr); + else + auto jvmdll = dlopen("libjvm.so"); + + if(jvmdll is null) + throw new Exception("no jvm dll"); + + version(Windows) + auto fn = cast(typeof(&JNI_CreateJavaVM)) GetProcAddress(jvmdll, "JNI_CreateJavaVM"); + else + auto fn = cast(typeof(&JNI_CreateJavaVM)) dlsym(jvmdll, "JNI_CreateJavaVM"); + + if(fn is null) + throw new Exception("no fun"); + + auto res = fn(&pvm, cast(void**) &env, &vm_args);//, args); if(res != JNI_OK) throw new Exception("create jvm failed"); // FIXME: throw res); activeJvm = pvm; - return JVM(ActivateJniEnv(env), pvm); + return JVM(ActivateJniEnv(env), pvm, jvmdll); } +version(Windows) +private extern(Windows) bool SetDllDirectoryW(wstring); +@JavaName("Throwable") +final class JavaThrowable : JavaClass!("java.lang", JavaThrowable) { + @Import string getMessage(); + @Import StackTraceElement[] getStackTrace(); +} + +final class StackTraceElement : JavaClass!("java.lang", StackTraceElement) { + @Import this(string declaringClass, string methodName, string fileName, int lineNumber); + @Import string getClassName(); + @Import string getFileName(); + @Import int getLineNumber(); + @Import string getMethodName(); + @Import bool isNativeMethod(); +} private void exceptionCheck(JNIEnv* env) { if((*env).ExceptionCheck(env)) { @@ -266,6 +406,7 @@ private void exceptionCheck(JNIEnv* env) { // do I need to free thrown? (*env).ExceptionClear(env); + // FIXME throw new Exception("Java threw"); } } @@ -274,6 +415,31 @@ private enum ImportImplementationString = q{ static if(is(typeof(return) == void)) { (*env).CallSTATICVoidMethod(env, jobj, _jmethodID, DDataToJni(env, args).args); exceptionCheck(env); + } else static if(is(typeof(return) == string) || is(typeof(return) == wstring)) { + // I can't just use JavaParamsToD here btw because of lifetime worries. + // maybe i should fix it down there though because there is a lot of duplication + + auto jret = (*env).CallSTATICObjectMethod(env, jobj, _jmethodID, DDataToJni(env, args).args); + exceptionCheck(env); + + typeof(return) ret; + + auto len = (*env).GetStringLength(env, jret); + auto ptr = (*env).GetStringChars(env, jret, null); + static if(is(T == wstring)) { + if(ptr !is null) { + ret = ptr[0 .. len].idup; + (*env).ReleaseStringChars(env, jret, ptr); + } + } else { + import std.conv; + if(ptr !is null) { + ret = to!string(ptr[0 .. len]); + (*env).ReleaseStringChars(env, jret, ptr); + } + } + + return ret; } else static if(is(typeof(return) == int)) { auto ret = (*env).CallSTATICIntMethod(env, jobj, _jmethodID, DDataToJni(env, args).args); exceptionCheck(env); @@ -302,13 +468,57 @@ private enum ImportImplementationString = q{ auto ret = (*env).CallSTATICCharMethod(env, jobj, _jmethodID, DDataToJni(env, args).args); exceptionCheck(env); return ret; + } else static if(is(typeof(return) == E[], E)) { + // Java arrays are represented as objects + auto jarr = (*env).CallSTATICObjectMethod(env, jobj, _jmethodID, DDataToJni(env, args).args); + exceptionCheck(env); + + auto len = (*env).GetArrayLength(env, jarr); + static if(is(E == int)) { + auto eles = (*env).GetIntArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseIntArrayElements(env, jarr, eles, 0); + } else static if(is(E == bool)) { + auto eles = (*env).GetBooleanArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseBooleanArrayElements(env, jarr, eles, 0); + } else static if(is(E == long)) { + auto eles = (*env).GetLongArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseLongArrayElements(env, jarr, eles, 0); + } else static if(is(E == short)) { + auto eles = (*env).GetShortArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseShortArrayElements(env, jarr, eles, 0); + } else static if(is(E == wchar)) { + auto eles = (*env).GetCharArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseCharArrayElements(env, jarr, eles, 0); + } else static if(is(E == float)) { + auto eles = (*env).GetFloatArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseFloatArrayElements(env, jarr, eles, 0); + } else static if(is(E == double)) { + auto eles = (*env).GetDoubleArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseDoubleArrayElements(env, jarr, eles, 0); + } else static if(is(E == byte)) { + auto eles = (*env).GetByteArrayElements(env, jarr, null); + auto res = eles[0 .. len]; + (*env).ReleaseByteArrayElements(env, jarr, eles, 0); + } else static if(is(E : IJavaObject)) { + // FIXME: implement this + typeof(return) res = null; + } else static assert(0, E.stringof ~ " not supported array element type yet"); // FIXME handle object arrays too. which would also prolly include arrays of arrays. + + return res; } else { static assert(0, "Unsupported return type for JNI " ~ typeof(return).stringof); //return DDataToJni(env, __traits(getMember, dobj, __traits(identifier, method))(JavaParamsToD!(Parameters!method)(env, args).args)); } }; -private mixin template JavaImportImpl(T, alias method) { +private mixin template JavaImportImpl(T, alias method, size_t overloadIndex) { import std.traits; private static jmethodID _jmethodID; @@ -362,7 +572,7 @@ private mixin template JavaImportImpl(T, alias method) { jc = internalJavaClassHandle_; } _jmethodID = (*env).GetStaticMethodID(env, jc, - __traits(identifier, method).ptr, + getJavaName!method.ptr, // java method string is (args)ret ("(" ~ DTypesToJniString!(typeof(args)) ~ ")" ~ DTypesToJniString!(typeof(return)) ~ "\0").ptr ); @@ -387,7 +597,7 @@ private mixin template JavaImportImpl(T, alias method) { if(!_jmethodID) { auto jc = (*env).GetObjectClass(env, jobj); _jmethodID = (*env).GetMethodID(env, jc, - __traits(identifier, method).ptr, + getJavaName!method.ptr, // java method string is (args)ret ("(" ~ DTypesToJniString!(typeof(args)) ~ ")" ~ DTypesToJniString!(typeof(return)) ~ "\0").ptr ); @@ -403,62 +613,49 @@ private mixin template JavaImportImpl(T, alias method) { private template DTypesToJniString(Types...) { static if(Types.length == 0) - string DTypesToJniString = ""; + enum string DTypesToJniString = ""; else static if(Types.length == 1) { alias T = Types[0]; static if(is(T == void)) - string DTypesToJniString = "V"; - else static if(is(T == string)) - string DTypesToJniString = "Ljava/lang/String;"; - else static if(is(T == wstring)) - string DTypesToJniString = "Ljava/lang/String;"; + enum string DTypesToJniString = "V"; + else static if(is(T == string)) { + enum string DTypesToJniString = "Ljava/lang/String;"; + } else static if(is(T == wstring)) + enum string DTypesToJniString = "Ljava/lang/String;"; else static if(is(T == int)) - string DTypesToJniString = "I"; + enum string DTypesToJniString = "I"; else static if(is(T == bool)) - string DTypesToJniString = "Z"; + enum string DTypesToJniString = "Z"; else static if(is(T == byte)) - string DTypesToJniString = "B"; + enum string DTypesToJniString = "B"; else static if(is(T == wchar)) - string DTypesToJniString = "C"; + enum string DTypesToJniString = "C"; else static if(is(T == short)) - string DTypesToJniString = "S"; + enum string DTypesToJniString = "S"; else static if(is(T == long)) - string DTypesToJniString = "J"; + enum string DTypesToJniString = "J"; else static if(is(T == float)) - string DTypesToJniString = "F"; + enum string DTypesToJniString = "F"; else static if(is(T == double)) - string DTypesToJniString = "D"; + enum string DTypesToJniString = "D"; else static if(is(T == size_t)) - string DTypesToJniString = "I"; // possible FIXME... + enum string DTypesToJniString = "I"; // possible FIXME... else static if(is(T == IJavaObject)) - string DTypesToJniString = "LObject;"; // FIXME? + enum string DTypesToJniString = "LObject;"; // FIXME? else static if(is(T : IJavaObject)) // child of this but a concrete type - string DTypesToJniString = T._javaParameterString; - /+ // FIXME they are just "[" ~ element type string - else static if(is(T == IJavaObject[])) - string DTypesToJniString = jobjectArray; - else static if(is(T == bool[])) - string DTypesToJniString = jbooleanArray; - else static if(is(T == byte[])) - string DTypesToJniString = jbyteArray; - else static if(is(T == wchar[])) - string DTypesToJniString = jcharArray; - else static if(is(T == short[])) - string DTypesToJniString = jshortArray; - else static if(is(T == int[])) - string DTypesToJniString = jintArray; - else static if(is(T == long[])) - string DTypesToJniString = jlongArray; - else static if(is(T == float[])) - string DTypesToJniString = jfloatArray; - else static if(is(T == double[])) - string DTypesToJniString = jdoubleArray; - +/ + enum string DTypesToJniString = T._javaParameterString; + else static if(is(T == E[], E)) + enum string DTypesToJniString = "[" ~ DTypesToJniString!E; else static assert(0, "Unsupported type for JNI call " ~ T.stringof); } else { - import std.typecons; - string DTypesToJni = DTypesToJni!(Types[0]) ~ DTypesToJni(Types[1 .. $]); + private string helper() { + string s; + foreach(Type; Types) + s ~= DTypesToJniString!Type; + return s; + } + enum string DTypesToJniString = helper; } } @@ -515,8 +712,10 @@ private template DTypesToJni(Types...) { alias DTypesToJni = jdoubleArray; else static assert(0, "Unsupported type for JNI " ~ T.stringof); } else { - import std.typecons; - alias DTypesToJni = AliasSeq!(DTypesToJni!(Types[0]), DTypesToJni(Types[1 .. $])); + import std.meta; + // FIXME: write about this later if you forget the ! on the final DTypesToJni, dmd + // says "error: recursive template expansion". woof. + alias DTypesToJni = AliasSeq!(DTypesToJni!(Types[0]), DTypesToJni!(Types[1 .. $])); } } @@ -562,7 +761,7 @@ auto DDatumToJni(T)(JNIEnv* env, T data) { else static if(is(T == float)) return data; else static if(is(T == double)) return data; else static if(is(T == size_t)) return cast(int) data; - else static if(is(T : IJavaObject)) return data.getJavaHandle(); + else static if(is(T : IJavaObject)) return data is null ? null : data.getJavaHandle(); else static assert(0, "Unsupported type " ~ T.stringof); /* // FIXME: finish these. else static if(is(T == IJavaObject[])) @@ -675,22 +874,24 @@ void jniRethrow(JNIEnv* env, Throwable t) { ); } -private mixin template JavaExportImpl(T, alias method) { +private mixin template JavaExportImpl(T, alias method, size_t overloadIndex) { import std.traits; import std.string; static if(__traits(identifier, method) == "__ctor") static assert(0, "Cannot export D constructors"); + /+ static private string JniMangle() { // this actually breaks with -betterC though so does a lot more so meh. static if(is(T : JavaClass!(JP, P), string JP, P)) return "Java_" ~replace(JP, ".", "_") ~ (JP.length ? "_" : "") ~ P.stringof ~ "_" ~ __traits(identifier, method); else static assert(0); } + +/ - extern(C) - pragma(mangle, JniMangle()) + extern(System) + //pragma(mangle, JniMangle()) // I need it in the DLL, but want it to be not accessible from outside... alas. export /*private*/ static DTypesToJni!(ReturnType!method) privateJniImplementation(JNIEnv* env, jobject obj, DTypesToJni!(Parameters!method) args) { // set it up in the thread for future calls @@ -716,19 +917,28 @@ private mixin template JavaExportImpl(T, alias method) { static if(is(typeof(return) == void)) { try { - __traits(getMember, dobj, __traits(identifier, method))(JavaParamsToD!(Parameters!method)(env, args).args); + __traits(getOverloads, dobj, __traits(identifier, method))[overloadIndex](JavaParamsToD!(Parameters!method)(env, args).args); } catch(Throwable t) { jniRethrow(env, t); } } else { try { - return DDatumToJni(env, __traits(getMember, dobj, __traits(identifier, method))(JavaParamsToD!(Parameters!method)(env, args).args)); + return DDatumToJni(env, __traits(getOverloads, dobj, __traits(identifier, method))[overloadIndex](JavaParamsToD!(Parameters!method)(env, args).args)); } catch(Throwable t) { jniRethrow(env, t); return typeof(return).init; // still required to return... } } } + + + shared static this() { + nativeMethodsData_ ~= JNINativeMethod( + getJavaName!method.ptr, + ("(" ~ DTypesToJniString!(Parameters!method) ~ ")" ~ DTypesToJniString!(ReturnType!method) ~ "\0").ptr, + &privateJniImplementation + ); + } } /++ @@ -776,27 +986,48 @@ class JavaClass(string javaPackage, CRTP) : IJavaObject { static assert("JavaClasses can only be constructed by Java. Try making a constructor in Java, then make an @Import this(args); here."); // implementations - static foreach(attr; __traits(getAttributes, __traits(getMember, CRTP, memberName))) { + static foreach(oi, overload; __traits(getOverloads, CRTP, memberName)) + static foreach(attr; __traits(getAttributes, overload)) { static if(is(attr == Import)) - mixin JavaImportImpl!(CRTP, __traits(getMember, CRTP, memberName)); + mixin JavaImportImpl!(CRTP, overload, oi); else static if(is(attr == Export)) - mixin JavaExportImpl!(CRTP, __traits(getMember, CRTP, memberName)); + mixin JavaExportImpl!(CRTP, overload, oi); } } protected jobject internalJavaHandle_; protected jobject getJavaHandle() { return internalJavaHandle_; } + __gshared static protected /*immutable*/ JNINativeMethod[] nativeMethodsData_; protected static jclass internalJavaClassHandle_; + protected static int initializeInJvm_(JNIEnv* env) { + + import core.stdc.stdio; + auto internalJavaClassHandle_ = (*env).FindClass(env, (_javaParameterString[1 .. $-1] ~ "\0").ptr); + if(!internalJavaClassHandle_) { + fprintf(stderr, ("Cannot find Java class for " ~ CRTP.stringof)); + return 1; + } + + if((*env).RegisterNatives(env, internalJavaClassHandle_, nativeMethodsData_.ptr, cast(int) nativeMethodsData_.length)) { + fprintf(stderr, ("RegisterNatives failed for " ~ CRTP.stringof)); + return 1; + } + return 0; + } + shared static this() { + classInitializers_ ~= &initializeInJvm_; + } static import std.string; static if(javaPackage.length) - public static immutable string _javaParameterString = "L" ~ std.string.replace(javaPackage, ".", "/") ~ "/" ~ CRTP.stringof ~ ";"; + public static immutable string _javaParameterString = "L" ~ std.string.replace(javaPackage, ".", "/") ~ "/" ~ getJavaName!CRTP ~ ";"; else - public static immutable string _javaParameterString = "L" ~ CRTP.stringof ~ ";"; + public static immutable string _javaParameterString = "L" ~ getJavaName!CRTP ~ ";"; } +__gshared /* immutable */ int function(JNIEnv* env)[] classInitializers_; @@ -820,7 +1051,7 @@ class JavaClass(string javaPackage, CRTP) : IJavaObject { import core.stdc.stdarg; //version (Android): -extern (C): +extern (System): @system: nothrow: @nogc: