Skip to content

Commit

Permalink
Add C# source generator for a new ScriptPath attribute
Browse files Browse the repository at this point in the history
This source generator adds a newly introduced attribute,
`ScriptPath` to all classes that:

- Are top-level classes (not inner/nested).
- Have the `partial` modifier.
- Inherit `Godot.Object`.
- The class name matches the file name.

A build error is thrown if the generator finds a class that meets these
conditions but is not declared `partial`, unless the class is annotated
with the `DisableGodotGenerators` attribute.

We also generate an `AssemblyHasScripts` assembly attribute which Godot
uses to get all the script classes in the assembly, eliminating the need
for Godot to search them. We can also avoid searching in assemblies that
don't have this attribute. This will be good for performance in the
future once we support multiple assemblies with Godot script classes.

This is an example of what the generated code looks like:

```
using Godot;
namespace Foo {
	[ScriptPathAttribute("res://Player.cs")]
	// Multiple partial declarations are allowed
	[ScriptPathAttribute("res://Foo/Player.cs")]
	partial class Player {}
}

[assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })]
```

The new attributes replace script metadata which we were generating by
determining the namespace of script classes with a very simple parser.
This fixes several issues with the old approach related to parser
errors and conditional compilation.
It also makes the task part of the MSBuild project build, rather than
a separate step executed by the Godot editor.
  • Loading branch information
neikeq committed Mar 6, 2021
1 parent d4191e4 commit e2afe70
Show file tree
Hide file tree
Showing 41 changed files with 652 additions and 1,350 deletions.
3 changes: 3 additions & 0 deletions modules/mono/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\SdkPackageVersions.props" />
</Project>
6 changes: 6 additions & 0 deletions modules/mono/SdkPackageVersions.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<PackageVersion_Godot_NET_Sdk>4.0.0-dev4</PackageVersion_Godot_NET_Sdk>
<PackageVersion_Godot_SourceGenerators>4.0.0-dev1</PackageVersion_Godot_SourceGenerators>
</PropertyGroup>
</Project>
24 changes: 17 additions & 7 deletions modules/mono/build_scripts/godot_net_sdk_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ def build_godot_net_sdk(source, target, env):
# No need to copy targets. The Godot.NET.Sdk csproj takes care of copying them.


def get_nupkgs_versions(props_file):
import xml.etree.ElementTree as ET

tree = ET.parse(props_file)
root = tree.getroot()

return {
"Godot.NET.Sdk": root.find("./PropertyGroup/PackageVersion_Godot_NET_Sdk").text.strip(),
"Godot.SourceGenerators": root.find("./PropertyGroup/PackageVersion_Godot_SourceGenerators").text.strip(),
}


def build(env_mono):
assert env_mono["tools"]

Expand All @@ -30,14 +42,12 @@ def build(env_mono):

module_dir = os.getcwd()

package_version_file = os.path.join(
module_dir, "editor", "Godot.NET.Sdk", "Godot.NET.Sdk", "Godot.NET.Sdk_PackageVersion.txt"
)

with open(package_version_file, mode="r") as f:
version = f.read().strip()
nupkgs_versions = get_nupkgs_versions(os.path.join(module_dir, "SdkPackageVersions.props"))

target_filenames = ["Godot.NET.Sdk.%s.nupkg" % version]
target_filenames = [
"Godot.NET.Sdk.%s.nupkg" % nupkgs_versions["Godot.NET.Sdk"],
"Godot.SourceGenerators.%s.nupkg" % nupkgs_versions["Godot.SourceGenerators"],
]

targets = [os.path.join(nupkgs_dir, filename) for filename in target_filenames]

Expand Down
120 changes: 60 additions & 60 deletions modules/mono/csharp_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "csharp_script.h"

#include <mono/metadata/threads.h>
#include <mono/metadata/tokentype.h>
#include <stdint.h>

#include "core/config/project_settings.h"
Expand Down Expand Up @@ -1182,46 +1183,56 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
}
#endif

void CSharpLanguage::_load_scripts_metadata() {
scripts_metadata.clear();
void CSharpLanguage::lookup_script_for_class(GDMonoClass *p_class) {
if (!p_class->has_attribute(CACHED_CLASS(ScriptPathAttribute))) {
return;
}

String scripts_metadata_filename = "scripts_metadata.";
MonoObject *attr = p_class->get_attribute(CACHED_CLASS(ScriptPathAttribute));
String path = CACHED_FIELD(ScriptPathAttribute, path)->get_string_value(attr);

#ifdef TOOLS_ENABLED
scripts_metadata_filename += Engine::get_singleton()->is_editor_hint() ? "editor" : "editor_player";
#else
#ifdef DEBUG_ENABLED
scripts_metadata_filename += "debug";
#else
scripts_metadata_filename += "release";
#endif
#endif
dotnet_script_lookup_map[path] = DotNetScriptLookupInfo(
p_class->get_namespace(), p_class->get_name(), p_class);
}

String scripts_metadata_path = GodotSharpDirs::get_res_metadata_dir().plus_file(scripts_metadata_filename);
void CSharpLanguage::lookup_scripts_in_assembly(GDMonoAssembly *p_assembly) {
if (p_assembly->has_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute))) {
MonoObject *attr = p_assembly->get_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute));
bool requires_lookup = CACHED_FIELD(AssemblyHasScriptsAttribute, requiresLookup)->get_bool_value(attr);

if (FileAccess::exists(scripts_metadata_path)) {
String old_json;
if (requires_lookup) {
// This is supported for scenarios where specifying all types would be cumbersome,
// such as when disabling C# source generators (for whatever reason) or when using a
// language other than C# that has nothing similar to source generators to automate it.
MonoImage *image = p_assembly->get_image();

Error ferr = read_all_file_utf8(scripts_metadata_path, old_json);
int rows = mono_image_get_table_rows(image, MONO_TABLE_TYPEDEF);

ERR_FAIL_COND(ferr != OK);
for (int i = 1; i < rows; i++) {
// We don't search inner classes, only top-level.
MonoClass *mono_class = mono_class_get(image, (i + 1) | MONO_TOKEN_TYPE_DEF);

Variant old_dict_var;
String err_str;
int err_line;
Error json_err = JSON::parse(old_json, old_dict_var, err_str, err_line);
if (json_err != OK) {
ERR_PRINT("Failed to parse metadata file: '" + err_str + "' (" + String::num_int64(err_line) + ").");
return;
}
if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) {
continue;
}

scripts_metadata = old_dict_var.operator Dictionary();
scripts_metadata_invalidated = false;
GDMonoClass *current = p_assembly->get_class(mono_class);
if (current) {
lookup_script_for_class(current);
}
}
} else {
// This is the most likely scenario as we use C# source generators
MonoArray *script_types = (MonoArray *)CACHED_FIELD(AssemblyHasScriptsAttribute, scriptTypes)->get_value(attr);

print_verbose("Successfully loaded scripts metadata");
} else {
if (!Engine::get_singleton()->is_editor_hint()) {
ERR_PRINT("Missing scripts metadata file.");
int length = mono_array_length(script_types);

for (int i = 0; i < length; i++) {
MonoReflectionType *reftype = mono_array_get(script_types, MonoReflectionType *, i);
ManagedType type = ManagedType::from_reftype(reftype);
ERR_CONTINUE(!type.type_class);
lookup_script_for_class(type.type_class);
}
}
}
}
Expand Down Expand Up @@ -1300,7 +1311,7 @@ void CSharpLanguage::_on_scripts_domain_unloaded() {
}
#endif

scripts_metadata_invalidated = true;
dotnet_script_lookup_map.clear();
}

#ifdef TOOLS_ENABLED
Expand Down Expand Up @@ -3356,45 +3367,34 @@ Error CSharpScript::reload(bool p_keep_state) {

GD_MONO_SCOPE_THREAD_ATTACH;

GDMonoAssembly *project_assembly = GDMono::get_singleton()->get_project_assembly();

if (project_assembly) {
const Variant *script_metadata_var = CSharpLanguage::get_singleton()->get_scripts_metadata().getptr(get_path());
if (script_metadata_var) {
Dictionary script_metadata = script_metadata_var->operator Dictionary()["class"];
const Variant *namespace_ = script_metadata.getptr("namespace");
const Variant *class_name = script_metadata.getptr("class_name");
ERR_FAIL_NULL_V(namespace_, ERR_BUG);
ERR_FAIL_NULL_V(class_name, ERR_BUG);
GDMonoClass *klass = project_assembly->get_class(namespace_->operator String(), class_name->operator String());
if (klass && CACHED_CLASS(GodotObject)->is_assignable_from(klass)) {
script_class = klass;
}
} else {
// Missing script metadata. Fallback to legacy method
script_class = project_assembly->get_object_derived_class(name);
const DotNetScriptLookupInfo *lookup_info =
CSharpLanguage::get_singleton()->lookup_dotnet_script(get_path());

if (lookup_info) {
GDMonoClass *klass = lookup_info->script_class;
if (klass) {
ERR_FAIL_COND_V(!CACHED_CLASS(GodotObject)->is_assignable_from(klass), FAILED);
script_class = klass;
}
}

valid = script_class != nullptr;
valid = script_class != nullptr;

if (script_class) {
if (script_class) {
#ifdef DEBUG_ENABLED
print_verbose("Found class " + script_class->get_full_name() + " for script " + get_path());
print_verbose("Found class " + script_class->get_full_name() + " for script " + get_path());
#endif

native = GDMonoUtils::get_class_native_base(script_class);
native = GDMonoUtils::get_class_native_base(script_class);

CRASH_COND(native == nullptr);
CRASH_COND(native == nullptr);

update_script_class_info(this);
update_script_class_info(this);

_update_exports();
}

return OK;
_update_exports();
}

return ERR_FILE_MISSING_DEPENDENCIES;
return OK;
}

ScriptLanguage *CSharpScript::get_language() const {
Expand Down
34 changes: 20 additions & 14 deletions modules/mono/csharp_script.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {

#define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst))

struct DotNetScriptLookupInfo {
String class_namespace;
String class_name;
GDMonoClass *script_class = nullptr;

DotNetScriptLookupInfo() {} // Required by HashMap...

DotNetScriptLookupInfo(const String &p_class_namespace, const String &p_class_name, GDMonoClass *p_script_class) :
class_namespace(p_class_namespace), class_name(p_class_name), script_class(p_script_class) {
}
};

class CSharpScript : public Script {
GDCLASS(CSharpScript, Script);

Expand Down Expand Up @@ -390,16 +402,15 @@ class CSharpLanguage : public ScriptLanguage {

int lang_idx = -1;

Dictionary scripts_metadata;
bool scripts_metadata_invalidated = true;
HashMap<String, DotNetScriptLookupInfo> dotnet_script_lookup_map;

void lookup_script_for_class(GDMonoClass *p_class);

// For debug_break and debug_break_parse
int _debug_parse_err_line = -1;
String _debug_parse_err_file;
String _debug_error;

void _load_scripts_metadata();

friend class GDMono;
void _on_scripts_domain_unloaded();

Expand Down Expand Up @@ -436,18 +447,13 @@ class CSharpLanguage : public ScriptLanguage {
void reload_assemblies(bool p_soft_reload);
#endif

_FORCE_INLINE_ Dictionary get_scripts_metadata_or_nothing() {
return scripts_metadata_invalidated ? Dictionary() : scripts_metadata;
}
_FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { return managed_callable_middleman; }

_FORCE_INLINE_ const Dictionary &get_scripts_metadata() {
if (scripts_metadata_invalidated) {
_load_scripts_metadata();
}
return scripts_metadata;
}
void lookup_scripts_in_assembly(GDMonoAssembly *p_assembly);

_FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { return managed_callable_middleman; }
const DotNetScriptLookupInfo *lookup_dotnet_script(const String &p_script_path) const {
return dotnet_script_lookup_map.getptr(p_script_path);
}

String get_name() const override;

Expand Down
18 changes: 18 additions & 0 deletions modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Sample", "Godot.SourceGenerators.Sample\Godot.SourceGenerators.Sample.csproj", "{7297A614-8DF5-43DE-9EAD-99671B26BD1F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -12,5 +18,17 @@ Global
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.Build.0 = Release|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.Build.0 = Release|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,33 @@

<PackageId>Godot.NET.Sdk</PackageId>
<Version>4.0.0</Version>
<PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl>
<PackageVersion>$(PackageVersion_Godot_NET_Sdk)</PackageVersion>
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</RepositoryUrl>
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageType>MSBuildSdk</PackageType>
<PackageTags>MSBuildSdk</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<PropertyGroup>
<NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile>
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn>
<!-- Exclude target framework from the package dependencies as we don't include the build output -->
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>

<Target Name="ReadGodotNETSdkVersion" BeforeTargets="BeforeBuild;BeforeRebuild;CoreCompile">
<PropertyGroup>
<PackageVersion>$([System.IO.File]::ReadAllText('$(ProjectDir)Godot.NET.Sdk_PackageVersion.txt').Trim())</PackageVersion>
</PropertyGroup>
</Target>
<ItemGroup>
<!-- Package Sdk\Sdk.props and Sdk\Sdk.targets file -->
<None Include="Sdk\Sdk.props" Pack="true" PackagePath="Sdk" Visible="false" />
<None Include="Sdk\Sdk.targets" Pack="true" PackagePath="Sdk" Visible="false" />
<!-- SdkPackageVersions.props -->

<Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') " DependsOnTargets="ReadGodotNETSdkVersion">
<PropertyGroup>
<NuspecProperties>
id=$(PackageId);
description=$(Description);
authors=$(Authors);
version=$(PackageVersion);
packagetype=$(PackageType);
tags=$(PackageTags);
projecturl=$(PackageProjectUrl)
</NuspecProperties>
</PropertyGroup>
</Target>
<None Include="..\..\..\SdkPackageVersions.props" Pack="true" PackagePath="Sdk" Visible="false" />
</ItemGroup>

<Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack">
<PropertyGroup>
<GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir>
</PropertyGroup>
<Copy SourceFiles="$(OutputPath)$(PackageId).$(PackageVersion).nupkg"
DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" />
<Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" />
</Target>
</Project>

This file was deleted.

This file was deleted.

Loading

0 comments on commit e2afe70

Please sign in to comment.