Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto generate C# bindings for gdextensions #8191

Open
tbillington opened this issue Oct 19, 2023 · 27 comments
Open

Auto generate C# bindings for gdextensions #8191

tbillington opened this issue Oct 19, 2023 · 27 comments

Comments

@tbillington
Copy link

tbillington commented Oct 19, 2023

Describe the project you are working on

3D multiplayer game targeting Steam using C#.

Mid-port from Unity.

Uses Steam Matchmaking/Networking to allow easy joining/inviting between friends, and Steam Datagram Relay (SDR) to achieve connections between players without port forwarding etc.

Describe the problem or limitation you are having in your project

Many important features and 3rd party integrations are implemented as GDExtensions in Godot. In our project: GodotSteam and SteamMultiplayerPeer.

However, with a csharp focused godot project these extensions are inaccessible.

We've written a GDScript "bridge" for for GodotSharp that exposes and proxies calls from CSharp, but it's time consuming and error prone.

Not having access to community extensions is a big limitation of Godot currently (for csharp developers/ex-unity).

Describe the feature / enhancement and how it helps to overcome the problem or limitation

With the continued focus on supporting csharp as a "first class" option in Godot, it follows that being able to access GDExtensions would be of increased importance.

Generating csharp bindings on extension "import" when csharp use is detected would bring csharp feature parity closer.

Ideally this would not be something extension developers have to individually and manually add to their extensions. It should be effortless/automatic, not requiring opt in. Otherwise I fear it just won't happen (the current situation).

Optional Extra

Support the work to allow generating bindings not just for csharp, but for other language extensions like rust and swift.

Achieving arbitrary language binding generation would greatly increase the capability of Godot as a multi-langauge extension and fully unlock it for developers from other languages.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

  1. GDExtensions would include their source when being packaged. (I assume source is required to generate csharp bindings)

  2. In the editor, upon download from the Godot Asset Library, Godot will generate csharp bindings and place them in the project, potentially alongside the extension in addons.

  3. Any required changes to .csproj or .sln would be made after generating the bindings.

If this enhancement will not be used often, can it be worked around with a few lines of script?

If users are not using Godot_mono, or in future don't have the csharp GDExtension enabled, generating bindings can be skipped.

Is there a reason why this should be core and not an add-on in the asset library?

I make this proposal here because csharp code is in core/the godot main repository.

@LauraWebdev
Copy link

Working with GDExtensions and C# certainly has been a bit cumbersome! Great proposal!

@TokisanGames
Copy link

TokisanGames commented Oct 19, 2023

However, with a csharp focused godot project these extensions are inaccessible.

Some of our C# users have helped us document how to use Terrain3D in C# and are using it without issue. See the "Detecting" and "Calling" sections. So GDextensions in C# may be inconvenient, but are not inaccessible.

@Bromeon
Copy link

Bromeon commented Oct 19, 2023

Is this information available in ClassDB? As such, access via reflection (as mentioned in the Terrain3D example) would always be a workaround:

     var terrain = ClassDB.Instantiate("Terrain3D");
     terrain.AsGodotObject().Set("storage", ClassDB.Instantiate("Terrain3DStorage"));
     terrain.AsGodotObject().Set("texture_list", ClassDB.Instantiate("Terrain3DTextureList"));
     terrain.AsGodotObject().Call("set_collision_enabled", true);

However, it would probably still be nice if Godot provided an option to include more classes in the C# code generation, as it already has the entire codegen infrastructure in place. This would allow type-safe and idiomatic APIs rather than dynamic ones.

Incidentally related: ongoing discussion of porting C# itself to GDExtension: #7895.

@raulsntos
Copy link
Member

GDExtension classes are registered in ClassDB and we can just retrieve the information needed from there. No need for the GDExtension source code.

As mentioned by @Bromeon, any GDExtension class can be consumed using the reflection-like APIs available in GodotObject (same as interop with GDScript classes). This means consuming GDExtensions is currently possible, but not ideal (because we lose type safety).

We do want to improve support for GDExtensions in C#, which includes creating them as well as consuming them.

If we were to use the codegen that exists in the editor (the bindings generator that generates the glue for GodotSharp), then there are a few things to keep in mind:

  • The bindings generator retrieves everything from ClassDB and generates the glue, we would probably need to refactor it so we can allow generating only certain classes (like the ones registered through GDExtensions).
  • We may be using internal APIs in the generated bindings source code. This would mean code generated in an user project would not compile.
  • The generated bindings source code use unsafe code blocks which would require user projects to also enable unsafe code blocks and, in my opinion, this is not acceptable.

Also, the proposed workflow assumes GDExtensions are added from the Asset Library using the editor to trigger the C# bindings generation. How would this work if an user places a GDExtension in their projects from outside the editor? For example, if I have a project in GitHub that I download which includes GDExtensions already.

I think the bindings generation should be part of the MSBuild compilation, either using Source Generators or MSBuild tasks. I mentioned this before in godot-rust/gdext#166 (comment).

@ChrisAbra
Copy link

ChrisAbra commented Oct 19, 2023

Would it make sense to have GDExtensions produce their own JSON API document similar to godot --dump-extension-api extension_api.json?

That way extensions could reference each other, rather than relying on the too-late ClassDB registration? Then its something build time for source generators to hook into and would be for more than just dotnet usage: extensions could reference each other.

@dsnopek
Copy link

dsnopek commented Oct 19, 2023

Would it make sense to have GDExtensions produce their own JSON API document similar to godot --dump-extension-api extension_api.json?

That way extensions could reference each other, rather than relying on the too-late ClassDB registration? Then its something build time for source generators to hook into and would be for more than just dotnet usage: extensions could reference each other.

This needs some testing, but this may already work to some degree.

If you run godot --dump-extension-api in a project that has some GDExtensions in it, it will actually put information for the classes from the GDExtensions into the extension_api.json. In theory, you should be able to then recompile your GDExtensions with this new JSON and it'll generate classes for them. I haven't actually ever tried this 2nd part, though :-)

But even if it does work, this isn't an ideal workflow, since it requires building all your GDExtensions, generating the extension_api.json and then re-building again (so, building twice).

@ChrisAbra
Copy link

ChrisAbra commented Oct 19, 2023

But even if it does work, this isn't an ideal workflow, since it requires building all your GDExtensions, generating the extension_api.json and then re-building again (so, building twice).

But in this instance of the C# bindings, the build is MSBuild right so assuming the Extensions are already built and registered we could use that as the source instead of ClassDB directly?

Edit: that's work thats already happening somewhere right? I've never been able to find where

@GeorgeS2019
Copy link

GeorgeS2019 commented Nov 28, 2023

@raulsntos

suggestion: distribute C# binding as nuget

The following step could only be done by the nuget provider using a GDExtension C# binding addon

Ideally

godot --dump-extension-api --gdextension will create each extension_api-gdextension-Name.json for each of the GDExtensions. Obviously, it is expected only one e.g. c++ GDExtension is present

The bindings generator retrieves everything from ClassDB and generates the glue, we would probably need to refactor it so we can allow generating only certain classes (like the ones registered through GDExtensions).

The user only consumes the nuget, no c# binding codes that require unsafe code compilation will be involved

We may be using internal APIs in the generated bindings source code. This would mean code generated in a user project would not compile.

The generated bindings source code uses unsafe code blocks which would require user projects to also enable unsafe code blocks and, in my opinion, this is not acceptable.

@Zetelias
Copy link

Zetelias commented Dec 9, 2023

Maybe we could modify these steps to work and do a script to automate them ?
They don't work with me at least

@GeorgeS2019
Copy link

GeorgeS2019 commented Jan 19, 2024

Update to this topic? When will this feature planned?

@DmitriySalnikov

Do you have a recommendation?

@DmitriySalnikov
Copy link

Do you have a recommendation?

Don't use ClassDB.Call/Set/Get in the core of the engine for C# bindings as I have.

Anyway, the bindings generator is already in the engine. It requires modifications, but this is most likely not a high priority.
I'm definitely not going to do that.

@GeorgeS2019
Copy link

@mihe => it seems ClassDB.Call/Set/Get is not recemmended

GodotObject.Get, GodotObject.Set and GodotObject.Call
godot-jolt/godot-jolt#632 (comment)

@mihe
Copy link

mihe commented Jan 23, 2024

it seems ClassDB.Call/Set/Get is not recemmended

As mentioned in the comment you linked, they're meant to be a workaround. They're pretty much the only alternative available from GDExtension right now, and as pointed out earlier in this thread the situation is similar to interop with GDScript, where you would also rely on GodotObject (or ClassDB I guess), as seen in the documentation.

@GeorgeS2019
Copy link

@DmitriySalnikov => elaborate?

Anyway, the bindings generator is already in the engine. It requires modifications

@raulsntos => care to share your latest view?

This issue has an impact on Unity Users using c# moving to Godot.

@DmitriySalnikov
Copy link

=> elaborate?

Just a little experiment that doesn't work

C# project, which should be placed in the root of your Godot project:
base extensions project.zip

Before building it, you need to build an editor with the patch, run GodotSharp.generate_extensions_bindings() in the editor (e.g. via EditorScript) and try to build godot project.

And don't forget to add .godot\mono\extension_bindings\bin\Debug\GodotSharpExtensions.dll to the project dependencies.

Patch for 4.2.1:

diff --git a/modules/mono/editor/bindings_generator.cpp b/modules/mono/editor/bindings_generator.cpp
index 36fdda46255d6dcb256619eab1b7d3619c0e3776..9afd060bbac98652f8fbcee95f458bfc42fe04b8 100644
--- a/modules/mono/editor/bindings_generator.cpp
+++ b/modules/mono/editor/bindings_generator.cpp
@@ -1228,6 +1228,113 @@ Error BindingsGenerator::generate_cs_core_project(const String &p_proj_dir) {
 	return OK;
 }
 
+Error BindingsGenerator::generate_cs_extensions_project(const String &p_proj_dir) {
+	ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
+
+	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	ERR_FAIL_COND_V(da.is_null(), ERR_CANT_CREATE);
+
+	if (!DirAccess::exists(p_proj_dir)) {
+		Error err = da->make_dir_recursive(p_proj_dir);
+		ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_CREATE, "Cannot create directory '" + p_proj_dir + "'.");
+	}
+
+	da->change_dir(p_proj_dir);
+	da->make_dir("Generated");
+	da->make_dir("Generated/GodotObjects");
+
+	String base_gen_dir = path::join(p_proj_dir, "Generated");
+	String godot_objects_gen_dir = path::join(base_gen_dir, "GodotObjects");
+
+	Vector<String> compile_items;
+
+	for (const KeyValue<StringName, TypeInterface> &E : obj_types) {
+		const TypeInterface &itype = E.value;
+
+		if (itype.api_type != ClassDB::API_EXTENSION) {
+			continue;
+		}
+
+		String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs");
+		Error err = _generate_cs_type(itype, output_file);
+
+		if (err == ERR_SKIP) {
+			continue;
+		}
+
+		if (err != OK) {
+			return err;
+		}
+
+		compile_items.push_back(output_file);
+	}
+
+	// Generate native calls
+
+	StringBuilder cs_icalls_content;
+
+	cs_icalls_content.append("namespace " BINDINGS_NAMESPACE ";\n\n");
+	cs_icalls_content.append("using System;\n"
+							 "using System.Diagnostics.CodeAnalysis;\n"
+							 "using System.Runtime.InteropServices;\n"
+							 "using Godot.NativeInterop;\n"
+							 "\n");
+	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]\n");
+	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantUnsafeContext\")]\n");
+	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantNameQualifier\")]\n");
+	cs_icalls_content.append("[System.Runtime.CompilerServices.SkipLocalsInit]\n");
+	cs_icalls_content.append("internal static class " BINDINGS_CLASS_NATIVECALLS_EXTENSION "\n{");
+
+	cs_icalls_content.append(MEMBER_BEGIN "internal static ulong godot_api_hash = ");
+	cs_icalls_content.append(String::num_uint64(ClassDB::get_api_hash(ClassDB::API_CORE)) + ";\n");
+
+	cs_icalls_content.append(MEMBER_BEGIN "private const int VarArgsSpanThreshold = 10;\n");
+
+	for (const InternalCall &icall : method_icalls) {
+		if (icall.api_type != ClassDB::API_EXTENSION) {
+			continue;
+		}
+		Error err = _generate_cs_native_calls(icall, cs_icalls_content);
+		if (err != OK) {
+			return err;
+		}
+	}
+
+	cs_icalls_content.append(CLOSE_BLOCK);
+
+	String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS_EXTENSION ".cs");
+
+	Error err = _save_file(internal_methods_file, cs_icalls_content);
+	if (err != OK) {
+		return err;
+	}
+
+	compile_items.push_back(internal_methods_file);
+
+	// Generate GeneratedIncludes.props
+
+	StringBuilder includes_props_content;
+	includes_props_content.append("<Project>\n"
+								  "  <ItemGroup>\n");
+
+	for (int i = 0; i < compile_items.size(); i++) {
+		String include = path::relative_to(compile_items[i], p_proj_dir).replace("/", "\\");
+		includes_props_content.append("    <Compile Include=\"" + include + "\" />\n");
+	}
+
+	includes_props_content.append("  </ItemGroup>\n"
+								  "</Project>\n");
+
+	String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props");
+
+	err = _save_file(includes_props_file, includes_props_content);
+	if (err != OK) {
+		return err;
+	}
+
+	return OK;
+}
+
 Error BindingsGenerator::generate_cs_editor_project(const String &p_proj_dir) {
 	ERR_FAIL_COND_V(!initialized, ERR_UNCONFIGURED);
 
@@ -2166,7 +2273,11 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
 				cs_type = cs_type.substr(0, cs_type.length() - 2);
 			}
 
-			String def_arg = sformat(iarg.default_argument, cs_type);
+			String def_arg_str = iarg.default_argument;
+			if (iarg.def_param_value.get_type() >= Variant::VECTOR2 && iarg.def_param_value.get_type() < Variant::PROJECTION)
+				def_arg_str = def_arg_str.replace("-inf", "real_t.NegativeInfinity").replace("inf", "real_t.PositiveInfinity");
+
+			String def_arg = sformat(def_arg_str, cs_type);
 
 			cs_in_statements << def_arg << ";\n";
 
@@ -2313,7 +2424,7 @@ Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterf
 
 		const InternalCall *im_icall = match->value;
 
-		String im_call = im_icall->editor_only ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS;
+		String im_call = im_icall->api_type == ClassDB::API_EXTENSION ? BINDINGS_CLASS_NATIVECALLS_EXTENSION : (im_icall->editor_only ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS);
 		im_call += ".";
 		im_call += im_icall->name;
 
diff --git a/modules/mono/editor/bindings_generator.h b/modules/mono/editor/bindings_generator.h
index aa4e5ea093bc4f80ab7e8b8659807b8065d40deb..d0b0db0cb4f7d4fca2b5c9e3ea46a9d9d1c9ea15 100644
--- a/modules/mono/editor/bindings_generator.h
+++ b/modules/mono/editor/bindings_generator.h
@@ -599,6 +599,7 @@ class BindingsGenerator {
 	struct InternalCall {
 		String name;
 		String unique_sig; // Unique signature to avoid duplicates in containers
+		ClassDB::APIType api_type = ClassDB::API_NONE;
 		bool editor_only = false;
 
 		bool is_vararg = false;
@@ -610,10 +611,11 @@ class BindingsGenerator {
 
 		InternalCall() {}
 
-		InternalCall(ClassDB::APIType api_type, const String &p_name, const String &p_unique_sig = String()) {
+		InternalCall(ClassDB::APIType p_api_type, const String &p_name, const String &p_unique_sig = String()) {
 			name = p_name;
 			unique_sig = p_unique_sig;
-			editor_only = api_type == ClassDB::API_EDITOR;
+			editor_only = p_api_type == ClassDB::API_EDITOR;
+			api_type = p_api_type;
 		}
 
 		inline bool operator==(const InternalCall &p_a) const {
@@ -820,6 +822,7 @@ class BindingsGenerator {
 
 public:
 	Error generate_cs_core_project(const String &p_proj_dir);
+	Error generate_cs_extensions_project(const String &p_proj_dir);
 	Error generate_cs_editor_project(const String &p_proj_dir);
 	Error generate_cs_api(const String &p_output_dir);
 
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs
index da6f293871089dbd16a5231d691dd124077da796..19beb385a3753f4648225197f99a8afc8942409e 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Properties/AssemblyInfo.cs
@@ -1,3 +1,4 @@
 using System.Runtime.CompilerServices;
 
 [assembly: InternalsVisibleTo("GodotSharpEditor")]
+[assembly: InternalsVisibleTo("GodotSharpExtensions")]
diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
index 3ceb3b64dc7ece60a774ae03803a7789b21c0d6f..c810c0fb150766314de83dcd657092d556f218b6 100644
--- a/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
+++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/GodotSharpEditor.csproj
@@ -37,6 +37,7 @@
   <!-- Compat Sources -->
   <ItemGroup>
     <Compile Include="Compat.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <!--
   We import a props file with auto-generated includes. This works well with Rider.
diff --git a/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs b/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000000000000000000000000000000000..3caf6f69df4f5a711b697e697ffd5a55bebe7786
--- /dev/null
+++ b/modules/mono/glue/GodotSharp/GodotSharpEditor/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("GodotSharpExtensions")]
diff --git a/modules/mono/godotsharp_defs.h b/modules/mono/godotsharp_defs.h
index 08eeffc3db1ac8b8fec48feab320742ec3e4e789..197d893998ca2aca44a4e85d6ef9395778c9a56c 100644
--- a/modules/mono/godotsharp_defs.h
+++ b/modules/mono/godotsharp_defs.h
@@ -39,6 +39,7 @@
 #define TOOLS_ASM_NAME "GodotTools"
 
 #define BINDINGS_CLASS_NATIVECALLS "NativeCalls"
+#define BINDINGS_CLASS_NATIVECALLS_EXTENSION "ExtensionsNativeCalls"
 #define BINDINGS_CLASS_NATIVECALLS_EDITOR "EditorNativeCalls"
 
 #endif // GODOTSHARP_DEFS_H
diff --git a/modules/mono/mono_gd/gd_mono.cpp b/modules/mono/mono_gd/gd_mono.cpp
index 3eb746677d581aa595aaae9b37c3c7859238b4c7..b4d7b897096c06982df4c8b42d2077cde6351bb0 100644
--- a/modules/mono/mono_gd/gd_mono.cpp
+++ b/modules/mono/mono_gd/gd_mono.cpp
@@ -39,6 +39,7 @@
 #include "gd_mono_cache.h"
 
 #ifdef TOOLS_ENABLED
+#include "../editor/bindings_generator.h"
 #include "../editor/hostfxr_resolver.h"
 #endif
 
@@ -574,6 +575,11 @@ bool GodotSharp::_is_runtime_initialized() {
 	return GDMono::get_singleton() != nullptr && GDMono::get_singleton()->is_runtime_initialized();
 }
 
+bool GodotSharp::_generate_extensions_bindings() {
+	BindingsGenerator generator;
+	return generator.generate_cs_extensions_project(".godot/mono/extension_bindings") == OK;
+}
+
 void GodotSharp::_reload_assemblies(bool p_soft_reload) {
 #ifdef GD_MONO_HOT_RELOAD
 	CRASH_COND(CSharpLanguage::get_singleton() == nullptr);
@@ -586,6 +592,7 @@ void GodotSharp::_reload_assemblies(bool p_soft_reload) {
 }
 
 void GodotSharp::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("generate_extensions_bindings"), &GodotSharp::_generate_extensions_bindings);
 	ClassDB::bind_method(D_METHOD("is_runtime_initialized"), &GodotSharp::_is_runtime_initialized);
 	ClassDB::bind_method(D_METHOD("_reload_assemblies"), &GodotSharp::_reload_assemblies);
 }
diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h
index 1b46a619ca37019aa8d2918de26a209b7aea09df..609f9728526ae0acc2ec4bd4163b91d17a2cab2f 100644
--- a/modules/mono/mono_gd/gd_mono.h
+++ b/modules/mono/mono_gd/gd_mono.h
@@ -169,6 +169,7 @@ class GodotSharp : public Object {
 
 	void _reload_assemblies(bool p_soft_reload);
 	bool _is_runtime_initialized();
+	bool _generate_extensions_bindings();
 
 protected:
 	static GodotSharp *singleton;

image

This patch allows you to generate a DLL that contains only bindings for all extensions. But my extensions could not run with this library, because in one case GodotObject could not be converted to my singleton, and in another case there were protected memory accesses. If anyone is interested and able to develop this patch into a complete PR, you're welcome to try.

@GeorgeS2019
Copy link

@DmitriySalnikov

Thnx so much. Now at least we can start where you left off to figure out more TOGETHER.

@GeorgeS2019
Copy link

@DmitriySalnikov

Can you provide?

Generated\ExtensionsNativeCalls.cs <= curious to see how this look like. this is not included in the provided Zip

@GeorgeS2019
Copy link

GeorgeS2019 commented Jan 25, 2024

Cpp

bindings_generator.cpp

https://github.com/godotengine/godot/blob/5034478611697358f4e135c69f364ef6d8cd54dc/modules/mono/editor/bindings_generator.cpp#L1232

Error BindingsGenerator::generate_cs_extensions_project(const String &p_proj_dir) {

        //Generate_cs_extensions files
	for (const KeyValue<StringName, TypeInterface> &E : obj_types) {
		const TypeInterface &itype = E.value;

		if (itype.api_type != ClassDB::API_EXTENSION) {
			continue;
		}

		String output_file = path::join(godot_objects_gen_dir, itype.proxy_name + ".cs");
		Error err = _generate_cs_type(itype, output_file);

		if (err == ERR_SKIP) {
			continue;
		}

		if (err != OK) {
			return err;
		}

		compile_items.push_back(output_file);
	}


	// Generate native calls

	StringBuilder cs_icalls_content;

	cs_icalls_content.append("namespace " BINDINGS_NAMESPACE ";\n\n");
	cs_icalls_content.append("using System;\n"
							 "using System.Diagnostics.CodeAnalysis;\n"
							 "using System.Runtime.InteropServices;\n"
							 "using Godot.NativeInterop;\n"
							 "\n");
	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"InconsistentNaming\")]\n");
	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantUnsafeContext\")]\n");
	cs_icalls_content.append("[SuppressMessage(\"ReSharper\", \"RedundantNameQualifier\")]\n");
	cs_icalls_content.append("[System.Runtime.CompilerServices.SkipLocalsInit]\n");
	cs_icalls_content.append("internal static class " BINDINGS_CLASS_NATIVECALLS_EXTENSION "\n{");

	cs_icalls_content.append(MEMBER_BEGIN "internal static ulong godot_api_hash = ");
	cs_icalls_content.append(String::num_uint64(ClassDB::get_api_hash(ClassDB::API_CORE)) + ";\n");

	cs_icalls_content.append(MEMBER_BEGIN "private const int VarArgsSpanThreshold = 10;\n");

	for (const InternalCall &icall : method_icalls) {
		if (icall.api_type != ClassDB::API_EXTENSION) {
			continue;
		}
		Error err = _generate_cs_native_calls(icall, cs_icalls_content);
		if (err != OK) {
			return err;
		}
	}

	cs_icalls_content.append(CLOSE_BLOCK);

	String internal_methods_file = path::join(base_gen_dir, BINDINGS_CLASS_NATIVECALLS_EXTENSION ".cs");

	Error err = _save_file(internal_methods_file, cs_icalls_content);
	if (err != OK) {
		return err;
	}

	compile_items.push_back(internal_methods_file);

       // Generate GeneratedIncludes.props

	StringBuilder includes_props_content;
	includes_props_content.append("<Project>\n"
								  "  <ItemGroup>\n");

	for (int i = 0; i < compile_items.size(); i++) {
		String include = path::relative_to(compile_items[i], p_proj_dir).replace("/", "\\");
		includes_props_content.append("    <Compile Include=\"" + include + "\" />\n");
	}

	includes_props_content.append("  </ItemGroup>\n"
								  "</Project>\n");

	String includes_props_file = path::join(base_gen_dir, "GeneratedIncludes.props");

	err = _save_file(includes_props_file, includes_props_content);
	if (err != OK) {
		return err;
	}

	return OK;
}

https://github.com/godotengine/godot/blob/5034478611697358f4e135c69f364ef6d8cd54dc/modules/mono/editor/bindings_generator.cpp#L2170

Error BindingsGenerator::_generate_cs_method(const BindingsGenerator::TypeInterface &p_itype, const BindingsGenerator::MethodInterface &p_imethod, int &p_method_bind_count, StringBuilder &p_output) {
	

	String def_arg_str = iarg.default_argument;
	if (iarg.def_param_value.get_type() >= Variant::VECTOR2 && iarg.def_param_value.get_type() < Variant::PROJECTION)
		def_arg_str = def_arg_str.replace("-inf", "real_t.NegativeInfinity").replace("inf", "real_t.PositiveInfinity");

	String def_arg = sformat(def_arg_str, cs_type);

}

(https://github.com/godotengine/godot/blob/5034478611697358f4e135c69f364ef6d8cd54dc/modules/mono/editor/bindings_generator.cpp#L2317)

-		String im_call = im_icall->editor_only ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS;
+		String im_call = im_icall->api_type == ClassDB::API_EXTENSION ? BINDINGS_CLASS_NATIVECALLS_EXTENSION : (im_icall->editor_only ? BINDINGS_CLASS_NATIVECALLS_EDITOR : BINDINGS_CLASS_NATIVECALLS);
bindings_generator.h

https://github.com/godotengine/godot/blob/5034478611697358f4e135c69f364ef6d8cd54dc/modules/mono/editor/bindings_generator.h#L601

	struct InternalCall {
		String name;
		String unique_sig; // Unique signature to avoid duplicates in containers
+		ClassDB::APIType api_type = ClassDB::API_NONE;
		bool editor_only = false;

https://github.com/godotengine/godot/blob/5034478611697358f4e135c69f364ef6d8cd54dc/modules/mono/editor/bindings_generator.h#L613

-		InternalCall(ClassDB::APIType api_type, const String &p_name, const String &p_unique_sig = String()) {
+		InternalCall(ClassDB::APIType p_api_type, const String &p_name, const String &p_unique_sig = String()) {
 			name = p_name;
 			unique_sig = p_unique_sig;
-			editor_only = api_type == ClassDB::API_EDITOR;
+			editor_only = p_api_type == ClassDB::API_EDITOR;
+			api_type = p_api_type;
 		}

 public:
 	Error generate_cs_core_project(const String &p_proj_dir);
+	Error generate_cs_extensions_project(const String &p_proj_dir);
 	Error generate_cs_editor_project(const String &p_proj_dir);
 	Error generate_cs_api(const String &p_output_dir);
godotsharp_defs.h https://github.com/godotengine/godot/blob/master/modules/mono/godotsharp_defs.h
 #define BINDINGS_CLASS_NATIVECALLS "NativeCalls"
+#define BINDINGS_CLASS_NATIVECALLS_EXTENSION "ExtensionsNativeCalls"
 #define BINDINGS_CLASS_NATIVECALLS_EDITOR "EditorNativeCalls"
gd_mono.cpp https://github.com/godotengine/godot/blob/master/modules/mono/mono_gd/gd_mono.cpp
 #ifdef TOOLS_ENABLED
+#include "../editor/bindings_generator.h"
 #include "../editor/hostfxr_resolver.h"
 #endif



+bool GodotSharp::_generate_extensions_bindings() {
+	BindingsGenerator generator;
+	return generator.generate_cs_extensions_project(".godot/mono/extension_bindings") == OK;
+}
+



void GodotSharp::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("generate_extensions_bindings"), &GodotSharp::_generate_extensions_bindings);
 	ClassDB::bind_method(D_METHOD("is_runtime_initialized"), &GodotSharp::_is_runtime_initialized);
 	ClassDB::bind_method(D_METHOD("_reload_assemblies"), &GodotSharp::_reload_assemblies);
 }
gd_mono.h https://github.com/godotengine/godot/blob/master/modules/mono/mono_gd/gd_mono.h
 	void _reload_assemblies(bool p_soft_reload);
 	bool _is_runtime_initialized();
+	bool _generate_extensions_bindings();

CSharp

GodotSharpExtensions
GodotSharp.csproj
GodotSharpEditor.csproj
[assembly: InternalsVisibleTo("GodotSharpExtensions")]

@GeorgeS2019
Copy link

Ideally, it needs to generate a second project GodotSharpExtensionsUnitTests.dll

@DmitriySalnikov
Copy link

Can you provide?

Generated\ExtensionsNativeCalls.cs <= curious to see how this look like. this is not included in the provided Zip

Nope. This file doesn't actually have to be included in the project directly. It's Generated\. It is created by the patched editor after calling GodotSharp.generate_extensions_bindings().

Cpp

bindings_generator.cpp

And why did you re-attach pieces of my code when I already attached the diff that applies to the git repository with a single command...? 🤷‍♂️

@GeorgeS2019
Copy link

Help myself and others to quickly have an overview what and where need to change, especially if better UI experience needed

@GeorgeS2019
Copy link

@DmitriySalnikov

I have been recently active in Godot VS2022 Test adaptor, which I think is a good test-driven development for Godot 4 including scene development.

Godot 4 C++ is not really my strength.

Anyone here has made further progress in realizing the GDExtension c# wrapper?

@GeorgeS2019
Copy link

Discussion from Discord

I went through the Godot c++ patch. It shows where in Godot Mono modules need to be adapted or modified to hijeck the existing Godot 4 glue code used to crate GodotSharp into => generating the wrappers for Gdextension.

The point, not to write your wrappers, but ===> Hiject the Godot's c# wrapper ENGINE into creating the GDExtension c# wrapper

The Godot4 c++ codes needed for modifications have been identified.

We need to share this so many could contribute how to make the c# wrapper generation process more robust and MOST importantly high performance.

Other more straight forwards methods will lead to low performance wrappers, From what I have gathered.

@mynameisfury First => start active on the thread #8191 => so the Godot team know there is strong interest

@lpares12
Copy link

Is there any current workaround to be able to extend a node registered in the gdextension from within C#? Even if it involves going through gdscript.

I tried this solution (that was previously linked to this post) DmitriySalnikov/godot_debug_draw_3d#2 (comment) and still C# can't manage to find those nodes.

@GeorgeS2019
Copy link

GeorgeS2019 commented Apr 6, 2024

@GeorgeS2019
Copy link

@GeorgeS2019
Copy link

Almost all the demo codes for c++ Gdextension are written in Gdscript

elamaunt/GDShrapt#2

The repos could help in writing integration tests for generated c# wrappers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests