From 7f32023a1ac60b62bd0159a542c25fdad0864dba Mon Sep 17 00:00:00 2001
From: Ruslan Mustakov <r.mustakov@gmail.com>
Date: Wed, 26 Jul 2017 15:58:12 +0700
Subject: Support multithreading for NativeScriptLanguage

Godot may call property setters from non-main thread when an object is
loaded in the edtior. This means NativeScriptLanguage could be accessed
from different threads, but it was not designed for thread-safety.
Besides, previous behaviour made it so that godot_nativescript_init and
godot_gdnative_init could be invoked from non-main thread, while
godot_gdnative_thread is always invoked on the main thread. This may
not be expected by the binding library.

This commit defers native library initialization to the main thread and
adds godot_nativescript_thread_enter and godot_nativescript_thread_exit
callbacks to make a binding library aware of foreign threads.
---
 modules/nativescript/nativescript.cpp   | 192 +++++++++++++++++++++++++-------
 modules/nativescript/nativescript.h     |  28 +++++
 modules/nativescript/register_types.cpp |  29 +++++
 3 files changed, 211 insertions(+), 38 deletions(-)

(limited to 'modules/nativescript')

diff --git a/modules/nativescript/nativescript.cpp b/modules/nativescript/nativescript.cpp
index fd83b74727..f3db33cce2 100644
--- a/modules/nativescript/nativescript.cpp
+++ b/modules/nativescript/nativescript.cpp
@@ -40,6 +40,10 @@
 #include "scene/main/scene_tree.h"
 #include "scene/resources/scene_format_text.h"
 
+#ifndef NO_THREADS
+#include "os/thread.h"
+#endif
+
 #if defined(TOOLS_ENABLED) && defined(DEBUG_METHODS_ENABLED)
 #include "api_generator.h"
 #endif
@@ -106,42 +110,16 @@ void NativeScript::set_library(Ref<GDNativeLibrary> p_library) {
 		return;
 	}
 	library = p_library;
-
-	// See if this library was "registered" already.
-
 	lib_path = library->get_active_library_path();
-	Map<String, Ref<GDNative> >::Element *E = NSL->library_gdnatives.find(lib_path);
-
-	if (!E) {
-		Ref<GDNative> gdn;
-		gdn.instance();
-		gdn->set_library(library);
-
-		// TODO(karroffel): check the return value?
-		gdn->initialize();
-
-		NSL->library_gdnatives.insert(lib_path, gdn);
 
-		NSL->library_classes.insert(lib_path, Map<StringName, NativeScriptDesc>());
-
-		if (!NSL->library_script_users.has(lib_path))
-			NSL->library_script_users.insert(lib_path, Set<NativeScript *>());
-
-		NSL->library_script_users[lib_path].insert(this);
-
-		void *args[1] = {
-			(void *)&lib_path
-		};
-
-		// here the library registers all the classes and stuff.
-		gdn->call_native_raw(NSL->_init_call_type,
-				NSL->_init_call_name,
-				NULL,
-				1,
-				args,
-				NULL);
-	} else {
-		// already initialized. Nice.
+#ifndef NO_THREADS
+	if (Thread::get_caller_ID() != Thread::get_main_ID()) {
+		NSL->defer_init_library(p_library, this);
+	} else
+#endif
+	{
+		NSL->init_library(p_library);
+		NSL->register_script(this);
 	}
 }
 
@@ -445,7 +423,7 @@ NativeScript::NativeScript() {
 
 // TODO(karroffel): implement this
 NativeScript::~NativeScript() {
-	NSL->library_script_users[lib_path].erase(this);
+	NSL->unregister_script(this);
 }
 
 ////// ScriptInstance stuff
@@ -798,6 +776,9 @@ void NativeScriptLanguage::_unload_stuff() {
 
 NativeScriptLanguage::NativeScriptLanguage() {
 	NativeScriptLanguage::singleton = this;
+#ifndef NO_THREADS
+	mutex = Mutex::create();
+#endif
 }
 
 // TODO(karroffel): implement this
@@ -811,6 +792,10 @@ NativeScriptLanguage::~NativeScriptLanguage() {
 		NSL->library_gdnatives.clear();
 		NSL->library_script_users.clear();
 	}
+
+#ifndef NO_THREADS
+	memdelete(mutex);
+#endif
 }
 
 String NativeScriptLanguage::get_name() const {
@@ -948,6 +933,134 @@ int NativeScriptLanguage::profiling_get_frame_data(ProfilingInfo *p_info_arr, in
 	return -1;
 }
 
+#ifndef NO_THREADS
+void NativeScriptLanguage::defer_init_library(Ref<GDNativeLibrary> lib, NativeScript *script) {
+	MutexLock lock(mutex);
+	libs_to_init.insert(lib);
+	scripts_to_register.insert(script);
+	has_objects_to_register = true;
+}
+#endif
+
+void NativeScriptLanguage::init_library(const Ref<GDNativeLibrary> &lib) {
+#ifndef NO_THREADS
+	MutexLock lock(mutex);
+#endif
+	// See if this library was "registered" already.
+	const String &lib_path = lib->get_active_library_path();
+	Map<String, Ref<GDNative> >::Element *E = library_gdnatives.find(lib_path);
+
+	if (!E) {
+		Ref<GDNative> gdn;
+		gdn.instance();
+		gdn->set_library(lib);
+
+		// TODO(karroffel): check the return value?
+		gdn->initialize();
+
+		library_gdnatives.insert(lib_path, gdn);
+
+		library_classes.insert(lib_path, Map<StringName, NativeScriptDesc>());
+
+		if (!library_script_users.has(lib_path))
+			library_script_users.insert(lib_path, Set<NativeScript *>());
+
+		void *args[1] = {
+			(void *)&lib_path
+		};
+
+		// here the library registers all the classes and stuff.
+		gdn->call_native_raw(_init_call_type,
+				_init_call_name,
+				NULL,
+				1,
+				args,
+				NULL);
+	} else {
+		// already initialized. Nice.
+	}
+}
+
+void NativeScriptLanguage::register_script(NativeScript *script) {
+#ifndef NO_THREADS
+	MutexLock lock(mutex);
+#endif
+	library_script_users[script->lib_path].insert(script);
+}
+
+void NativeScriptLanguage::unregister_script(NativeScript *script) {
+#ifndef NO_THREADS
+	MutexLock lock(mutex);
+#endif
+	Map<String, Set<NativeScript *> >::Element *S = library_script_users.find(script->lib_path);
+	if (S) {
+		S->get().erase(script);
+		if (S->get().size() == 0) {
+			library_script_users.erase(S);
+		}
+	}
+#ifndef NO_THREADS
+	scripts_to_register.erase(script);
+#endif
+}
+
+#ifndef NO_THREADS
+
+void NativeScriptLanguage::frame() {
+	if (has_objects_to_register) {
+		MutexLock lock(mutex);
+		for (Set<Ref<GDNativeLibrary> >::Element *L = libs_to_init.front(); L; L = L->next()) {
+			init_library(L->get());
+		}
+		libs_to_init.clear();
+		for (Set<NativeScript *>::Element *S = scripts_to_register.front(); S; S = S->next()) {
+			register_script(S->get());
+		}
+		scripts_to_register.clear();
+		has_objects_to_register = false;
+	}
+}
+
+void NativeScriptLanguage::thread_enter() {
+	Vector<Ref<GDNative> > libs;
+	{
+		MutexLock lock(mutex);
+		for (Map<String, Ref<GDNative> >::Element *L = library_gdnatives.front(); L; L = L->next()) {
+			libs.push_back(L->get());
+		}
+	}
+	for (int i = 0; i < libs.size(); ++i) {
+		libs[i]->call_native_raw(
+				_thread_cb_call_type,
+				_thread_enter_call_name,
+				NULL,
+				0,
+				NULL,
+				NULL);
+	}
+}
+
+void NativeScriptLanguage::thread_exit() {
+	Vector<Ref<GDNative> > libs;
+	{
+		MutexLock lock(mutex);
+		for (Map<String, Ref<GDNative> >::Element *L = library_gdnatives.front(); L; L = L->next()) {
+			libs.push_back(L->get());
+		}
+	}
+	for (int i = 0; i < libs.size(); ++i) {
+		libs[i]->call_native_raw(
+				_thread_cb_call_type,
+				_thread_exit_call_name,
+				NULL,
+				0,
+				NULL,
+				NULL);
+	}
+}
+
+#endif // NO_THREADS
+
 void NativeReloadNode::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_notification"), &NativeReloadNode::_notification);
 }
@@ -960,7 +1073,9 @@ void NativeReloadNode::_notification(int p_what) {
 
 			if (unloaded)
 				break;
-
+#ifndef NO_THREADS
+			MutexLock lock(NSL->mutex);
+#endif
 			NSL->_unload_stuff();
 			for (Map<String, Ref<GDNative> >::Element *L = NSL->library_gdnatives.front(); L; L = L->next()) {
 
@@ -976,9 +1091,10 @@ void NativeReloadNode::_notification(int p_what) {
 
 			if (!unloaded)
 				break;
-
+#ifndef NO_THREADS
+			MutexLock lock(NSL->mutex);
+#endif
 			Set<StringName> libs_to_remove;
-
 			for (Map<String, Ref<GDNative> >::Element *L = NSL->library_gdnatives.front(); L; L = L->next()) {
 
 				if (!L->get()->initialize()) {
diff --git a/modules/nativescript/nativescript.h b/modules/nativescript/nativescript.h
index bc7a6e3ed6..cf3a64e9b8 100644
--- a/modules/nativescript/nativescript.h
+++ b/modules/nativescript/nativescript.h
@@ -41,6 +41,10 @@
 #include "godot_nativescript.h"
 #include "modules/gdnative/gdnative.h"
 
+#ifndef NO_THREADS
+#include "os/mutex.h"
+#endif
+
 struct NativeScriptDesc {
 
 	struct Method {
@@ -197,6 +201,19 @@ private:
 
 	void _unload_stuff();
 
+#ifndef NO_THREADS
+	Mutex *mutex;
+
+	Set<Ref<GDNativeLibrary> > libs_to_init;
+	Set<NativeScript *> scripts_to_register;
+	volatile bool has_objects_to_register; // so that we don't lock mutex every frame - it's rarely needed
+	void defer_init_library(Ref<GDNativeLibrary> lib, NativeScript *script);
+#endif
+
+	void init_library(const Ref<GDNativeLibrary> &lib);
+	void register_script(NativeScript *script);
+	void unregister_script(NativeScript *script);
+
 public:
 	Map<String, Map<StringName, NativeScriptDesc> > library_classes;
 	Map<String, Ref<GDNative> > library_gdnatives;
@@ -206,6 +223,10 @@ public:
 	const StringName _init_call_type = "nativescript_init";
 	const StringName _init_call_name = "godot_nativescript_init";
 
+	const StringName _thread_cb_call_type = "godot_nativescript_thread_cb";
+	const StringName _thread_enter_call_name = "godot_nativescript_thread_enter";
+	const StringName _thread_exit_call_name = "godot_nativescript_thread_exit";
+
 	NativeScriptLanguage();
 	~NativeScriptLanguage();
 
@@ -215,6 +236,13 @@ public:
 
 	void _hacky_api_anchor();
 
+#ifndef NO_THREADS
+	virtual void thread_enter();
+	virtual void thread_exit();
+
+	virtual void frame();
+#endif
+
 	virtual String get_name() const;
 	virtual void init();
 	virtual String get_type() const;
diff --git a/modules/nativescript/register_types.cpp b/modules/nativescript/register_types.cpp
index 6c88b04a56..a8a931343b 100644
--- a/modules/nativescript/register_types.cpp
+++ b/modules/nativescript/register_types.cpp
@@ -61,6 +61,32 @@ void init_call_cb(void *p_handle, godot_string *p_proc_name, void *p_data, int p
 	fn(args[0]);
 }
 
+#ifndef NO_THREADS
+
+typedef void (*native_script_empty_callback)();
+
+void thread_call_cb(void *p_handle, godot_string *p_proc_name, void *p_data, int p_num_args, void **args, void *r_ret) {
+	if (p_handle == NULL) {
+		ERR_PRINT("No valid library handle, can't call nativescript thread enter/exit callback");
+		return;
+	}
+
+	void *library_proc;
+	Error err = OS::get_singleton()->get_dynamic_library_symbol_handle(
+			p_handle,
+			*(String *)p_proc_name,
+			library_proc);
+	if (err != OK) {
+		// it's fine if thread callbacks are not present in the library.
+		return;
+	}
+
+	native_script_empty_callback fn = (native_script_empty_callback)library_proc;
+	fn();
+}
+
+#endif // NO_THREADS
+
 ResourceFormatLoaderNativeScript *resource_loader_gdns = NULL;
 ResourceFormatSaverNativeScript *resource_saver_gdns = NULL;
 
@@ -72,6 +98,9 @@ void register_nativescript_types() {
 	ScriptServer::register_language(native_script_language);
 
 	GDNativeCallRegistry::singleton->register_native_raw_call_type(native_script_language->_init_call_type, init_call_cb);
+#ifndef NO_THREADS
+	GDNativeCallRegistry::singleton->register_native_raw_call_type(native_script_language->_thread_cb_call_type, thread_call_cb);
+#endif
 
 	resource_saver_gdns = memnew(ResourceFormatSaverNativeScript);
 	ResourceSaver::add_resource_format_saver(resource_saver_gdns);
-- 
cgit v1.2.3