소스 검색

Merge pull request #37304 from m4gr3d/implement_plugin_signals

Add signal support to Godot Android plugin
Rémi Verschelde 5 년 전
부모
커밋
ae42cb7b0a

+ 5 - 0
platform/android/api/api.cpp

@@ -32,6 +32,7 @@
 
 #include "core/engine.h"
 #include "java_class_wrapper.h"
+#include "jni_singleton.h"
 
 #if !defined(ANDROID_ENABLED)
 static JavaClassWrapper *java_class_wrapper = nullptr;
@@ -40,7 +41,11 @@ static JavaClassWrapper *java_class_wrapper = nullptr;
 void register_android_api() {
 
 #if !defined(ANDROID_ENABLED)
+	// On Android platforms, the `java_class_wrapper` instantiation and the
+	// `JNISingleton` registration occurs in
+	// `platform/android/java_godot_lib_jni.cpp#Java_org_godotengine_godot_GodotLib_setup`
 	java_class_wrapper = memnew(JavaClassWrapper); // Dummy
+	ClassDB::register_class<JNISingleton>();
 #endif
 
 	ClassDB::register_class<JavaClass>();

+ 242 - 0
platform/android/api/jni_singleton.h

@@ -0,0 +1,242 @@
+/*************************************************************************/
+/*  jni_singleton.h                                                      */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef JNI_SINGLETON_H
+#define JNI_SINGLETON_H
+
+#include <core/engine.h>
+#include <core/variant.h>
+#ifdef ANDROID_ENABLED
+#include <platform/android/jni_utils.h>
+#endif
+
+class JNISingleton : public Object {
+
+	GDCLASS(JNISingleton, Object);
+
+#ifdef ANDROID_ENABLED
+	struct MethodData {
+
+		jmethodID method;
+		Variant::Type ret_type;
+		Vector<Variant::Type> argtypes;
+	};
+
+	jobject instance;
+	Map<StringName, MethodData> method_map;
+#endif
+
+public:
+	virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
+#ifdef ANDROID_ENABLED
+		Map<StringName, MethodData>::Element *E = method_map.find(p_method);
+
+		// Check the method we're looking for is in the JNISingleton map and that
+		// the arguments match.
+		bool call_error = !E || E->get().argtypes.size() != p_argcount;
+		if (!call_error) {
+			for (int i = 0; i < p_argcount; i++) {
+
+				if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
+					call_error = true;
+					break;
+				}
+			}
+		}
+
+		if (call_error) {
+			// The method is not in this map, defaulting to the regular instance calls.
+			return Object::call(p_method, p_args, p_argcount, r_error);
+		}
+
+		ERR_FAIL_COND_V(!instance, Variant());
+
+		r_error.error = Callable::CallError::CALL_OK;
+
+		jvalue *v = nullptr;
+
+		if (p_argcount) {
+
+			v = (jvalue *)alloca(sizeof(jvalue) * p_argcount);
+		}
+
+		JNIEnv *env = ThreadAndroid::get_env();
+
+		int res = env->PushLocalFrame(16);
+
+		ERR_FAIL_COND_V(res != 0, Variant());
+
+		List<jobject> to_erase;
+		for (int i = 0; i < p_argcount; i++) {
+
+			jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]);
+			v[i] = vr.val;
+			if (vr.obj)
+				to_erase.push_back(vr.obj);
+		}
+
+		Variant ret;
+
+		switch (E->get().ret_type) {
+
+			case Variant::NIL: {
+
+				env->CallVoidMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::BOOL: {
+
+				ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE;
+			} break;
+			case Variant::INT: {
+
+				ret = env->CallIntMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::FLOAT: {
+
+				ret = env->CallFloatMethodA(instance, E->get().method, v);
+			} break;
+			case Variant::STRING: {
+
+				jobject o = env->CallObjectMethodA(instance, E->get().method, v);
+				ret = jstring_to_string((jstring)o, env);
+				env->DeleteLocalRef(o);
+			} break;
+			case Variant::PACKED_STRING_ARRAY: {
+
+				jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				ret = _jobject_to_variant(env, arr);
+
+				env->DeleteLocalRef(arr);
+			} break;
+			case Variant::PACKED_INT32_ARRAY: {
+
+				jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				int fCount = env->GetArrayLength(arr);
+				Vector<int> sarr;
+				sarr.resize(fCount);
+
+				int *w = sarr.ptrw();
+				env->GetIntArrayRegion(arr, 0, fCount, w);
+				ret = sarr;
+				env->DeleteLocalRef(arr);
+			} break;
+			case Variant::PACKED_FLOAT32_ARRAY: {
+
+				jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v);
+
+				int fCount = env->GetArrayLength(arr);
+				Vector<float> sarr;
+				sarr.resize(fCount);
+
+				float *w = sarr.ptrw();
+				env->GetFloatArrayRegion(arr, 0, fCount, w);
+				ret = sarr;
+				env->DeleteLocalRef(arr);
+			} break;
+
+#ifndef _MSC_VER
+#warning This is missing 64 bits arrays, I have no idea how to do it in JNI
+#endif
+			case Variant::DICTIONARY: {
+
+				jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
+				ret = _jobject_to_variant(env, obj);
+				env->DeleteLocalRef(obj);
+
+			} break;
+			default: {
+
+				env->PopLocalFrame(nullptr);
+				ERR_FAIL_V(Variant());
+			} break;
+		}
+
+		while (to_erase.size()) {
+			env->DeleteLocalRef(to_erase.front()->get());
+			to_erase.pop_front();
+		}
+
+		env->PopLocalFrame(nullptr);
+
+		return ret;
+#else // ANDROID_ENABLED
+
+		// Defaulting to the regular instance calls.
+		return Object::call(p_method, p_args, p_argcount, r_error);
+#endif
+	}
+
+#ifdef ANDROID_ENABLED
+	jobject get_instance() const {
+
+		return instance;
+	}
+
+	void set_instance(jobject p_instance) {
+
+		instance = p_instance;
+	}
+
+	void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
+
+		MethodData md;
+		md.method = p_method;
+		md.argtypes = p_args;
+		md.ret_type = p_ret_type;
+		method_map[p_name] = md;
+	}
+
+	void add_signal(const StringName &p_name, const Vector<Variant::Type> &p_args) {
+		if (p_args.size() == 0)
+			ADD_SIGNAL(MethodInfo(p_name));
+		else if (p_args.size() == 1)
+			ADD_SIGNAL(MethodInfo(p_name, PropertyInfo(p_args[0], "arg1")));
+		else if (p_args.size() == 2)
+			ADD_SIGNAL(MethodInfo(p_name, PropertyInfo(p_args[0], "arg1"), PropertyInfo(p_args[1], "arg2")));
+		else if (p_args.size() == 3)
+			ADD_SIGNAL(MethodInfo(p_name, PropertyInfo(p_args[0], "arg1"), PropertyInfo(p_args[1], "arg2"), PropertyInfo(p_args[2], "arg3")));
+		else if (p_args.size() == 4)
+			ADD_SIGNAL(MethodInfo(p_name, PropertyInfo(p_args[0], "arg1"), PropertyInfo(p_args[1], "arg2"), PropertyInfo(p_args[2], "arg3"), PropertyInfo(p_args[3], "arg4")));
+		else if (p_args.size() == 5)
+			ADD_SIGNAL(MethodInfo(p_name, PropertyInfo(p_args[0], "arg1"), PropertyInfo(p_args[1], "arg2"), PropertyInfo(p_args[2], "arg3"), PropertyInfo(p_args[3], "arg4"), PropertyInfo(p_args[4], "arg5")));
+	}
+
+#endif
+
+	JNISingleton() {
+#ifdef ANDROID_ENABLED
+		instance = nullptr;
+#endif
+	}
+};
+
+#endif // JNI_SINGLETON_H

+ 84 - 1
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java

@@ -34,6 +34,8 @@ import android.app.Activity;
 import android.content.Intent;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
 import android.view.Surface;
 import android.view.View;
 import java.lang.reflect.Method;
@@ -41,8 +43,10 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
+import org.godotengine.godot.BuildConfig;
 import org.godotengine.godot.Godot;
 
 /**
@@ -70,7 +74,10 @@ import org.godotengine.godot.Godot;
  */
 public abstract class GodotPlugin {
 
+	private static final String TAG = GodotPlugin.class.getSimpleName();
+
 	private final Godot godot;
+	private final ConcurrentHashMap<String, SignalInfo> registeredSignals = new ConcurrentHashMap<>();
 
 	public GodotPlugin(Godot godot) {
 		this.godot = godot;
@@ -118,6 +125,13 @@ public abstract class GodotPlugin {
 			nativeRegisterMethod(getPluginName(), method.getName(), method.getReturnType().getName(), pt);
 		}
 
+		// Register the signals for this plugin.
+		for (SignalInfo signalInfo : getPluginSignals()) {
+			String signalName = signalInfo.getName();
+			nativeRegisterSignal(getPluginName(), signalName, signalInfo.getParamTypesNames());
+			registeredSignals.put(signalName, signalInfo);
+		}
+
 		// Get the list of gdnative libraries to register.
 		Set<String> gdnativeLibrariesPaths = getPluginGDNativeLibrariesPaths();
 		if (!gdnativeLibrariesPaths.isEmpty()) {
@@ -220,7 +234,17 @@ public abstract class GodotPlugin {
 	 * Returns the list of methods to be exposed to Godot.
 	 */
 	@NonNull
-	public abstract List<String> getPluginMethods();
+	public List<String> getPluginMethods() {
+		return Collections.emptyList();
+	}
+
+	/**
+	 * Returns the list of signals to be exposed to Godot.
+	 */
+	@NonNull
+	public Set<SignalInfo> getPluginSignals() {
+		return Collections.emptySet();
+	}
 
 	/**
 	 * Returns the paths for the plugin's gdnative libraries.
@@ -252,6 +276,49 @@ public abstract class GodotPlugin {
 		godot.runOnRenderThread(action);
 	}
 
+	/**
+	 * Emit a registered Godot signal.
+	 * @param signalName
+	 * @param signalArgs
+	 */
+	protected void emitSignal(final String signalName, final Object... signalArgs) {
+		try {
+			// Check that the given signal is among the registered set.
+			SignalInfo signalInfo = registeredSignals.get(signalName);
+			if (signalInfo == null) {
+				throw new IllegalArgumentException(
+						"Signal " + signalName + " is not registered for this plugin.");
+			}
+
+			// Validate the arguments count.
+			Class<?>[] signalParamTypes = signalInfo.getParamTypes();
+			if (signalArgs.length != signalParamTypes.length) {
+				throw new IllegalArgumentException(
+						"Invalid arguments count. Should be " + signalParamTypes.length + "  but is " + signalArgs.length);
+			}
+
+			// Validate the argument's types.
+			for (int i = 0; i < signalParamTypes.length; i++) {
+				if (!signalParamTypes[i].isInstance(signalArgs[i])) {
+					throw new IllegalArgumentException(
+							"Invalid type for argument #" + i + ". Should be of type " + signalParamTypes[i].getName());
+				}
+			}
+
+			runOnRenderThread(new Runnable() {
+				@Override
+				public void run() {
+					nativeEmitSignal(getPluginName(), signalName, signalArgs);
+				}
+			});
+		} catch (IllegalArgumentException exception) {
+			Log.w(TAG, exception.getMessage());
+			if (BuildConfig.DEBUG) {
+				throw exception;
+			}
+		}
+	}
+
 	/**
 	 * Used to setup a {@link GodotPlugin} instance.
 	 * @param p_name Name of the instance.
@@ -272,4 +339,20 @@ public abstract class GodotPlugin {
 	 * @param gdnlibPaths Paths to the libraries relative to the 'assets' directory.
 	 */
 	private native void nativeRegisterGDNativeLibraries(String[] gdnlibPaths);
+
+	/**
+	 * Used to complete registration of the {@link GodotPlugin} instance's methods.
+	 * @param pluginName Name of the plugin
+	 * @param signalName Name of the signal to register
+	 * @param signalParamTypes Signal parameters types
+	 */
+	private native void nativeRegisterSignal(String pluginName, String signalName, String[] signalParamTypes);
+
+	/**
+	 * Used to emit signal by {@link GodotPlugin} instance.
+	 * @param pluginName Name of the plugin
+	 * @param signalName Name of the signal to emit
+	 * @param signalParams Signal parameters
+	 */
+	private native void nativeEmitSignal(String pluginName, String signalName, Object[] signalParams);
 }

+ 98 - 0
platform/android/java/lib/src/org/godotengine/godot/plugin/SignalInfo.java

@@ -0,0 +1,98 @@
+/*************************************************************************/
+/*  SignalInfo.java                                                      */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md)    */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.plugin;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import java.util.Arrays;
+
+/**
+ * Store information about a {@link GodotPlugin}'s signal.
+ */
+public final class SignalInfo {
+
+	private final String name;
+	private final Class<?>[] paramTypes;
+	private final String[] paramTypesNames;
+
+	public SignalInfo(@NonNull String signalName, Class<?>... paramTypes) {
+		if (TextUtils.isEmpty(signalName)) {
+			throw new IllegalArgumentException("Invalid signal name: " + signalName);
+		}
+
+		this.name = signalName;
+		this.paramTypes = paramTypes == null ? new Class<?>[ 0 ] : paramTypes;
+		this.paramTypesNames = new String[this.paramTypes.length];
+		for (int i = 0; i < this.paramTypes.length; i++) {
+			this.paramTypesNames[i] = this.paramTypes[i].getName();
+		}
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	Class<?>[] getParamTypes() {
+		return paramTypes;
+	}
+
+	String[] getParamTypesNames() {
+		return paramTypesNames;
+	}
+
+	@Override
+	public String toString() {
+		return "SignalInfo{"
+				+
+				"name='" + name + '\'' +
+				", paramsTypes=" + Arrays.toString(paramTypes) +
+				'}';
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (this == o) {
+			return true;
+		}
+		if (!(o instanceof SignalInfo)) {
+			return false;
+		}
+
+		SignalInfo that = (SignalInfo)o;
+
+		return name.equals(that.name);
+	}
+
+	@Override
+	public int hashCode() {
+		return name.hashCode();
+	}
+}

+ 2 - 0
platform/android/java_godot_lib_jni.cpp

@@ -35,6 +35,7 @@
 
 #include "android/asset_manager_jni.h"
 #include "api/java_class_wrapper.h"
+#include "api/jni_singleton.h"
 #include "audio_driver_jandroid.h"
 #include "core/engine.h"
 #include "core/input/input_filter.h"
@@ -162,6 +163,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
 	}
 
 	java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity()));
+	ClassDB::register_class<JNISingleton>();
 }
 
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height) {

+ 0 - 186
platform/android/jni_utils.h

@@ -53,190 +53,4 @@ Variant::Type get_jni_type(const String &p_type);
 
 const char *get_jni_sig(const String &p_type);
 
-class JNISingleton : public Object {
-
-	GDCLASS(JNISingleton, Object);
-
-	struct MethodData {
-
-		jmethodID method;
-		Variant::Type ret_type;
-		Vector<Variant::Type> argtypes;
-	};
-
-	jobject instance;
-	Map<StringName, MethodData> method_map;
-
-public:
-	virtual Variant call(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
-
-		ERR_FAIL_COND_V(!instance, Variant());
-
-		r_error.error = Callable::CallError::CALL_OK;
-
-		Map<StringName, MethodData>::Element *E = method_map.find(p_method);
-		if (!E) {
-
-			r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD;
-			return Variant();
-		}
-
-		int ac = E->get().argtypes.size();
-		if (ac < p_argcount) {
-
-			r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
-			r_error.argument = ac;
-			return Variant();
-		}
-
-		if (ac > p_argcount) {
-
-			r_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
-			r_error.argument = ac;
-			return Variant();
-		}
-
-		for (int i = 0; i < p_argcount; i++) {
-
-			if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) {
-
-				r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT;
-				r_error.argument = i;
-				r_error.expected = E->get().argtypes[i];
-			}
-		}
-
-		jvalue *v = nullptr;
-
-		if (p_argcount) {
-
-			v = (jvalue *)alloca(sizeof(jvalue) * p_argcount);
-		}
-
-		JNIEnv *env = ThreadAndroid::get_env();
-
-		int res = env->PushLocalFrame(16);
-
-		ERR_FAIL_COND_V(res != 0, Variant());
-
-		List<jobject> to_erase;
-		for (int i = 0; i < p_argcount; i++) {
-
-			jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]);
-			v[i] = vr.val;
-			if (vr.obj)
-				to_erase.push_back(vr.obj);
-		}
-
-		Variant ret;
-
-		switch (E->get().ret_type) {
-
-			case Variant::NIL: {
-
-				env->CallVoidMethodA(instance, E->get().method, v);
-			} break;
-			case Variant::BOOL: {
-
-				ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE;
-			} break;
-			case Variant::INT: {
-
-				ret = env->CallIntMethodA(instance, E->get().method, v);
-			} break;
-			case Variant::FLOAT: {
-
-				ret = env->CallFloatMethodA(instance, E->get().method, v);
-			} break;
-			case Variant::STRING: {
-
-				jobject o = env->CallObjectMethodA(instance, E->get().method, v);
-				ret = jstring_to_string((jstring)o, env);
-				env->DeleteLocalRef(o);
-			} break;
-			case Variant::PACKED_STRING_ARRAY: {
-
-				jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v);
-
-				ret = _jobject_to_variant(env, arr);
-
-				env->DeleteLocalRef(arr);
-			} break;
-			case Variant::PACKED_INT32_ARRAY: {
-
-				jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v);
-
-				int fCount = env->GetArrayLength(arr);
-				Vector<int> sarr;
-				sarr.resize(fCount);
-
-				int *w = sarr.ptrw();
-				env->GetIntArrayRegion(arr, 0, fCount, w);
-				ret = sarr;
-				env->DeleteLocalRef(arr);
-			} break;
-			case Variant::PACKED_FLOAT32_ARRAY: {
-
-				jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v);
-
-				int fCount = env->GetArrayLength(arr);
-				Vector<float> sarr;
-				sarr.resize(fCount);
-
-				float *w = sarr.ptrw();
-				env->GetFloatArrayRegion(arr, 0, fCount, w);
-				ret = sarr;
-				env->DeleteLocalRef(arr);
-			} break;
-
-#ifndef _MSC_VER
-#warning This is missing 64 bits arrays, I have no idea how to do it in JNI
-#endif
-			case Variant::DICTIONARY: {
-
-				jobject obj = env->CallObjectMethodA(instance, E->get().method, v);
-				ret = _jobject_to_variant(env, obj);
-				env->DeleteLocalRef(obj);
-
-			} break;
-			default: {
-
-				env->PopLocalFrame(nullptr);
-				ERR_FAIL_V(Variant());
-			} break;
-		}
-
-		while (to_erase.size()) {
-			env->DeleteLocalRef(to_erase.front()->get());
-			to_erase.pop_front();
-		}
-
-		env->PopLocalFrame(nullptr);
-
-		return ret;
-	}
-
-	jobject get_instance() const {
-
-		return instance;
-	}
-	void set_instance(jobject p_instance) {
-
-		instance = p_instance;
-	}
-
-	void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) {
-
-		MethodData md;
-		md.method = p_method;
-		md.argtypes = p_args;
-		md.ret_type = p_ret_type;
-		method_map[p_name] = md;
-	}
-
-	JNISingleton() {
-		instance = nullptr;
-	}
-};
-
 #endif // JNI_UTILS_H

+ 47 - 1
platform/android/plugin/godot_plugin_jni.cpp

@@ -33,6 +33,7 @@
 #include <core/engine.h>
 #include <core/error_macros.h>
 #include <core/project_settings.h>
+#include <platform/android/api/jni_singleton.h>
 #include <platform/android/jni_utils.h>
 #include <platform/android/string_android.h>
 
@@ -43,7 +44,7 @@ extern "C" {
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jobject obj, jstring name) {
 
 	String singname = jstring_to_string(name, env);
-	JNISingleton *s = memnew(JNISingleton);
+	JNISingleton *s = (JNISingleton *)ClassDB::instance("JNISingleton");
 	s->set_instance(env->NewGlobalRef(obj));
 	jni_singletons[singname] = s;
 
@@ -86,6 +87,51 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis
 	s->add_method(mname, mid, types, get_jni_type(retval));
 }
 
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jobject obj, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types) {
+	String singleton_name = jstring_to_string(j_plugin_name, env);
+
+	ERR_FAIL_COND(!jni_singletons.has(singleton_name));
+
+	JNISingleton *singleton = jni_singletons.get(singleton_name);
+
+	String signal_name = jstring_to_string(j_signal_name, env);
+	Vector<Variant::Type> types;
+
+	int stringCount = env->GetArrayLength(j_signal_param_types);
+
+	for (int i = 0; i < stringCount; i++) {
+
+		jstring j_signal_param_type = (jstring)env->GetObjectArrayElement(j_signal_param_types, i);
+		const String signal_param_type = jstring_to_string(j_signal_param_type, env);
+		types.push_back(get_jni_type(signal_param_type));
+	}
+
+	singleton->add_signal(signal_name, types);
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitSignal(JNIEnv *env, jobject obj, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_params) {
+	String singleton_name = jstring_to_string(j_plugin_name, env);
+
+	ERR_FAIL_COND(!jni_singletons.has(singleton_name));
+
+	JNISingleton *singleton = jni_singletons.get(singleton_name);
+
+	String signal_name = jstring_to_string(j_signal_name, env);
+
+	int count = env->GetArrayLength(j_signal_params);
+	const Variant *args[count];
+
+	for (int i = 0; i < count; i++) {
+
+		jobject j_param = env->GetObjectArrayElement(j_signal_params, i);
+		Variant variant = _jobject_to_variant(env, j_param);
+		args[i] = &variant;
+		env->DeleteLocalRef(j_param);
+	};
+
+	singleton->emit_signal(signal_name, args, count);
+}
+
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths) {
 	int gdnlib_count = env->GetArrayLength(gdnlib_paths);
 	if (gdnlib_count == 0) {

+ 2 - 0
platform/android/plugin/godot_plugin_jni.h

@@ -37,6 +37,8 @@
 extern "C" {
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSingleton(JNIEnv *env, jobject obj, jstring name);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jobject obj, jstring sname, jstring name, jstring ret, jobjectArray args);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jobject obj, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitSignal(JNIEnv *env, jobject obj, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_params);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDNativeLibraries(JNIEnv *env, jobject obj, jobjectArray gdnlib_paths);
 }