Submodule Plugins/UnrealSharp deleted from a5e501bc43
2
Plugins/UnrealSharp/.gitattributes
vendored
Normal file
2
Plugins/UnrealSharp/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
84
Plugins/UnrealSharp/.gitignore
vendored
Normal file
84
Plugins/UnrealSharp/.gitignore
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
# Visual Studio 2015 user specific files
|
||||
.vs/
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.ipa
|
||||
|
||||
# These project files can be generated by the engine
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
*.suo
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.VC.db
|
||||
*.VC.opendb
|
||||
|
||||
# Precompiled Assets
|
||||
SourceArt/**/*.png
|
||||
SourceArt/**/*.tga
|
||||
|
||||
# Binary Files
|
||||
Binaries/*
|
||||
Plugins/*/Binaries/*
|
||||
|
||||
# Builds
|
||||
Build/*
|
||||
|
||||
# Whitelist PakBlacklist-<BuildConfiguration>.txt files
|
||||
!Build/*/
|
||||
Build/*/**
|
||||
!Build/*/PakBlacklist*.txt
|
||||
|
||||
# Don't ignore icon files in Build
|
||||
!Build/**/*.ico
|
||||
|
||||
# Built data for maps
|
||||
*_BuiltData.uasset
|
||||
|
||||
# Configuration files generated by the Editor
|
||||
Saved/*
|
||||
|
||||
# Compiled source files for the engine to use
|
||||
Intermediate/*
|
||||
Plugins/*/Intermediate/*
|
||||
|
||||
# Cache files for the editor to use
|
||||
DerivedDataCache/*
|
||||
|
||||
*.user
|
||||
|
||||
.idea/
|
||||
|
||||
bin/
|
||||
Generated/
|
||||
obj/
|
||||
|
||||
*.props
|
||||
!Directory.Packages.props
|
||||
117
Plugins/UnrealSharp/Config/Default.UnrealSharpTypes.json
Normal file
117
Plugins/UnrealSharp/Config/Default.UnrealSharpTypes.json
Normal file
@ -0,0 +1,117 @@
|
||||
{
|
||||
"Structs": {
|
||||
"CustomTypes": [
|
||||
"FInstancedStruct"
|
||||
],
|
||||
"BlittableTypes": [
|
||||
{
|
||||
"Name": "EStreamingSourcePriority"
|
||||
},
|
||||
{
|
||||
"Name": "ETriggerEvent"
|
||||
},
|
||||
{
|
||||
"Name": "FVector",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector"
|
||||
},
|
||||
{
|
||||
"Name": "FVector2D",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector2D"
|
||||
},
|
||||
{
|
||||
"Name": "FVector_NetQuantize",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector"
|
||||
},
|
||||
{
|
||||
"Name": "FVector_NetQuantize10",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector"
|
||||
},
|
||||
{
|
||||
"Name": "FVector_NetQuantize100",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector"
|
||||
},
|
||||
{
|
||||
"Name": "FVector_NetQuantizeNormal",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector"
|
||||
},
|
||||
{
|
||||
"Name": "FVector2f",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector2f"
|
||||
},
|
||||
{
|
||||
"Name": "FVector3f",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector3f"
|
||||
},
|
||||
{
|
||||
"Name": "FVector4f",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector4f"
|
||||
},
|
||||
{
|
||||
"Name": "FQuatf4",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FVector4f"
|
||||
},
|
||||
{
|
||||
"Name": "FRotator",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FRotator"
|
||||
},
|
||||
{
|
||||
"Name": "FMatrix44f",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FMatrix44f"
|
||||
},
|
||||
{
|
||||
"Name": "FTransform",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FTransform"
|
||||
},
|
||||
{
|
||||
"Name": "FTimerHandle",
|
||||
"ManagedType": "UnrealSharp.Engine.FTimerHandle"
|
||||
},
|
||||
{
|
||||
"Name": "FInputActionValue",
|
||||
"ManagedType": "UnrealSharp.EnhancedInput.FInputActionValue"
|
||||
},
|
||||
{
|
||||
"Name": "FRandomStream",
|
||||
"ManagedType": "UnrealSharp.CoreUObject.FRandomStream"
|
||||
},
|
||||
{
|
||||
"Name": "FSubsystemCollectionBaseRef",
|
||||
"ManagedType": "UnrealSharp.UnrealSharpCore.FSubsystemCollectionBaseRef"
|
||||
}
|
||||
],
|
||||
"NativelyTranslatableTypes": [
|
||||
{
|
||||
"Name": "FMoverDataCollection",
|
||||
"HasDestructor": false
|
||||
},
|
||||
{
|
||||
"Name": "FPaintContext",
|
||||
"HasDestructor": false
|
||||
},
|
||||
{
|
||||
"Name": "FGeometry",
|
||||
"HasDestructor": false
|
||||
},
|
||||
{
|
||||
"Name": "FGameplayEffectSpec",
|
||||
"HasDestructor": true
|
||||
},
|
||||
{
|
||||
"Name": "FGameplayEffectSpecHandle",
|
||||
"HasDestructor": true
|
||||
},
|
||||
{
|
||||
"Name": "FKeyEvent",
|
||||
"HasDestructor": false
|
||||
},
|
||||
{
|
||||
"Name": "FInputEvent",
|
||||
"HasDestructor": false
|
||||
},
|
||||
{
|
||||
"Name": "FKey",
|
||||
"HasDestructor": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
Plugins/UnrealSharp/Config/DefaultUnrealSharp.ini
Normal file
5
Plugins/UnrealSharp/Config/DefaultUnrealSharp.ini
Normal file
@ -0,0 +1,5 @@
|
||||
[CoreRedirects]
|
||||
+ClassRedirects=(OldName="/Script/CSharpForUE.CSClass",NewName="/Script/UnrealSharpCore.CSClass")
|
||||
+StructRedirects=(OldName="/Script/CSharpForUE.CSScriptStruct",NewName="/Script/UnrealSharpCore.CSScriptStruct")
|
||||
+EnumRedirects=(OldName="/Script/CSharpForUE.CSEnum",NewName="/Script/UnrealSharpCore.CSEnum")
|
||||
+ClassRedirects=(OldName="/Script/UnrealSharpCore.CSDeveloperSettings",NewName="/Script/UnrealSharpCore.CSUnrealSharpSettings")
|
||||
8
Plugins/UnrealSharp/Config/FilterPlugin.ini
Normal file
8
Plugins/UnrealSharp/Config/FilterPlugin.ini
Normal file
@ -0,0 +1,8 @@
|
||||
[FilterPlugin]
|
||||
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
|
||||
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
|
||||
;
|
||||
; Examples:
|
||||
; /README.txt
|
||||
; /Extras/...
|
||||
; /Binaries/ThirdParty/*.dll
|
||||
18
Plugins/UnrealSharp/Directory.Packages.props
Normal file
18
Plugins/UnrealSharp/Directory.Packages.props
Normal file
@ -0,0 +1,18 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
|
||||
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.13.9" />
|
||||
<PackageVersion Include="Microsoft.Build" Version="17.13.9" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.Build.Locator" Version="1.9.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
21
Plugins/UnrealSharp/LICENSE
Normal file
21
Plugins/UnrealSharp/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 UnrealSharp
|
||||
|
||||
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.
|
||||
@ -0,0 +1,47 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#ifndef __CORECLR_DELEGATES_H__
|
||||
#define __CORECLR_DELEGATES_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define CORECLR_DELEGATE_CALLTYPE __stdcall
|
||||
#ifdef _WCHAR_T_DEFINED
|
||||
typedef wchar_t char_t;
|
||||
#else
|
||||
typedef unsigned short char_t;
|
||||
#endif
|
||||
#else
|
||||
#define CORECLR_DELEGATE_CALLTYPE
|
||||
typedef char char_t;
|
||||
#endif
|
||||
|
||||
#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1)
|
||||
|
||||
// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_fn)(
|
||||
const char_t *assembly_path /* Fully qualified path to assembly */,
|
||||
const char_t *type_name /* Assembly qualified type name */,
|
||||
const char_t *method_name /* Public static method name compatible with delegateType */,
|
||||
const char_t *delegate_type_name /* Assembly qualified delegate type name or null
|
||||
or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
|
||||
the UnmanagedCallersOnlyAttribute. */,
|
||||
void *reserved /* Extensibility parameter (currently unused and must be 0) */,
|
||||
/*out*/ void **delegate /* Pointer where to store the function pointer result */);
|
||||
|
||||
// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default)
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes);
|
||||
|
||||
typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)(
|
||||
const char_t *type_name /* Assembly qualified type name */,
|
||||
const char_t *method_name /* Public static method name compatible with delegateType */,
|
||||
const char_t *delegate_type_name /* Assembly qualified delegate type name or null,
|
||||
or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
|
||||
the UnmanagedCallersOnlyAttribute. */,
|
||||
void *load_context /* Extensibility parameter (currently unused and must be 0) */,
|
||||
void *reserved /* Extensibility parameter (currently unused and must be 0) */,
|
||||
/*out*/ void **delegate /* Pointer where to store the function pointer result */);
|
||||
|
||||
#endif // __CORECLR_DELEGATES_H__
|
||||
323
Plugins/UnrealSharp/Managed/DotNetRuntime/inc/hostfxr.h
Normal file
323
Plugins/UnrealSharp/Managed/DotNetRuntime/inc/hostfxr.h
Normal file
@ -0,0 +1,323 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#ifndef __HOSTFXR_H__
|
||||
#define __HOSTFXR_H__
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define HOSTFXR_CALLTYPE __cdecl
|
||||
#ifdef _WCHAR_T_DEFINED
|
||||
typedef wchar_t char_t;
|
||||
#else
|
||||
typedef unsigned short char_t;
|
||||
#endif
|
||||
#else
|
||||
#define HOSTFXR_CALLTYPE
|
||||
typedef char char_t;
|
||||
#endif
|
||||
|
||||
enum hostfxr_delegate_type
|
||||
{
|
||||
hdt_com_activation,
|
||||
hdt_load_in_memory_assembly,
|
||||
hdt_winrt_activation,
|
||||
hdt_com_register,
|
||||
hdt_com_unregister,
|
||||
hdt_load_assembly_and_get_function_pointer,
|
||||
hdt_get_function_pointer,
|
||||
};
|
||||
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv);
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_startupinfo_fn)(
|
||||
const int argc,
|
||||
const char_t **argv,
|
||||
const char_t *host_path,
|
||||
const char_t *dotnet_root,
|
||||
const char_t *app_path);
|
||||
typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)(
|
||||
const int argc,
|
||||
const char_t** argv,
|
||||
const char_t* host_path,
|
||||
const char_t* dotnet_root,
|
||||
const char_t* app_path,
|
||||
int64_t bundle_header_offset);
|
||||
|
||||
typedef void(HOSTFXR_CALLTYPE *hostfxr_error_writer_fn)(const char_t *message);
|
||||
|
||||
//
|
||||
// Sets a callback which is to be used to write errors to.
|
||||
//
|
||||
// Parameters:
|
||||
// error_writer
|
||||
// A callback function which will be invoked every time an error is to be reported.
|
||||
// Or nullptr to unregister previously registered callback and return to the default behavior.
|
||||
// Return value:
|
||||
// The previously registered callback (which is now unregistered), or nullptr if no previous callback
|
||||
// was registered
|
||||
//
|
||||
// The error writer is registered per-thread, so the registration is thread-local. On each thread
|
||||
// only one callback can be registered. Subsequent registrations overwrite the previous ones.
|
||||
//
|
||||
// By default no callback is registered in which case the errors are written to stderr.
|
||||
//
|
||||
// Each call to the error writer is sort of like writing a single line (the EOL character is omitted).
|
||||
// Multiple calls to the error writer may occure for one failure.
|
||||
//
|
||||
// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer
|
||||
// will be propagated to hostpolicy for the duration of the call. This means that errors from
|
||||
// both hostfxr and hostpolicy will be reporter through the same error writer.
|
||||
//
|
||||
typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE *hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer);
|
||||
|
||||
typedef void* hostfxr_handle;
|
||||
struct hostfxr_initialize_parameters
|
||||
{
|
||||
size_t size;
|
||||
const char_t *host_path;
|
||||
const char_t *dotnet_root;
|
||||
};
|
||||
|
||||
//
|
||||
// Initializes the hosting components for a dotnet command line running an application
|
||||
//
|
||||
// Parameters:
|
||||
// argc
|
||||
// Number of argv arguments
|
||||
// argv
|
||||
// Command-line arguments for running an application (as if through the dotnet executable).
|
||||
// Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported.
|
||||
// For example 'app.dll app_argument_1 app_argument_2`.
|
||||
// parameters
|
||||
// Optional. Additional parameters for initialization
|
||||
// host_context_handle
|
||||
// On success, this will be populated with an opaque value representing the initialized host context
|
||||
//
|
||||
// Return value:
|
||||
// Success - Hosting components were successfully initialized
|
||||
// HostInvalidState - Hosting components are already initialized
|
||||
//
|
||||
// This function parses the specified command-line arguments to determine the application to run. It will
|
||||
// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and
|
||||
// dependencies and prepare everything needed to load the runtime.
|
||||
//
|
||||
// This function only supports arguments for running an application. It does not support SDK commands.
|
||||
//
|
||||
// This function does not load the runtime.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_dotnet_command_line_fn)(
|
||||
int argc,
|
||||
const char_t **argv,
|
||||
const struct hostfxr_initialize_parameters *parameters,
|
||||
/*out*/ hostfxr_handle *host_context_handle);
|
||||
|
||||
//
|
||||
// Initializes the hosting components using a .runtimeconfig.json file
|
||||
//
|
||||
// Parameters:
|
||||
// runtime_config_path
|
||||
// Path to the .runtimeconfig.json file
|
||||
// parameters
|
||||
// Optional. Additional parameters for initialization
|
||||
// host_context_handle
|
||||
// On success, this will be populated with an opaque value representing the initialized host context
|
||||
//
|
||||
// Return value:
|
||||
// Success - Hosting components were successfully initialized
|
||||
// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components
|
||||
// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components
|
||||
// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components
|
||||
//
|
||||
// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed
|
||||
// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that
|
||||
// may be next to the .runtimeconfig.json).
|
||||
//
|
||||
// This function does not load the runtime.
|
||||
//
|
||||
// If called when the runtime has already been loaded, this function will check if the specified runtime
|
||||
// config is compatible with the existing runtime.
|
||||
//
|
||||
// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful
|
||||
// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that
|
||||
// the difference in properties is acceptable.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_runtime_config_fn)(
|
||||
const char_t *runtime_config_path,
|
||||
const struct hostfxr_initialize_parameters *parameters,
|
||||
/*out*/ hostfxr_handle *host_context_handle);
|
||||
|
||||
//
|
||||
// Gets the runtime property value for an initialized host context
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
// name
|
||||
// Runtime property name
|
||||
// value
|
||||
// Out parameter. Pointer to a buffer with the property value.
|
||||
//
|
||||
// Return value:
|
||||
// The error code result.
|
||||
//
|
||||
// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only
|
||||
// guaranteed until any of the below occur:
|
||||
// - a 'run' method is called for the host context
|
||||
// - properties are changed via hostfxr_set_runtime_property_value
|
||||
// - the host context is closed via 'hostfxr_close'
|
||||
//
|
||||
// If host_context_handle is nullptr and an active host context exists, this function will get the
|
||||
// property value for the active host context.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_property_value_fn)(
|
||||
const hostfxr_handle host_context_handle,
|
||||
const char_t *name,
|
||||
/*out*/ const char_t **value);
|
||||
|
||||
//
|
||||
// Sets the value of a runtime property for an initialized host context
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
// name
|
||||
// Runtime property name
|
||||
// value
|
||||
// Value to set
|
||||
//
|
||||
// Return value:
|
||||
// The error code result.
|
||||
//
|
||||
// Setting properties is only supported for the first host context, before the runtime has been loaded.
|
||||
//
|
||||
// If the property already exists in the host context, it will be overwritten. If value is nullptr, the
|
||||
// property will be removed.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_set_runtime_property_value_fn)(
|
||||
const hostfxr_handle host_context_handle,
|
||||
const char_t *name,
|
||||
const char_t *value);
|
||||
|
||||
//
|
||||
// Gets all the runtime properties for an initialized host context
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
// count
|
||||
// [in] Size of the keys and values buffers
|
||||
// [out] Number of properties returned (size of keys/values buffers used). If the input value is too
|
||||
// small or keys/values is nullptr, this is populated with the number of available properties
|
||||
// keys
|
||||
// Array of pointers to buffers with runtime property keys
|
||||
// values
|
||||
// Array of pointers to buffers with runtime property values
|
||||
//
|
||||
// Return value:
|
||||
// The error code result.
|
||||
//
|
||||
// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only
|
||||
// guaranteed until any of the below occur:
|
||||
// - a 'run' method is called for the host context
|
||||
// - properties are changed via hostfxr_set_runtime_property_value
|
||||
// - the host context is closed via 'hostfxr_close'
|
||||
//
|
||||
// If host_context_handle is nullptr and an active host context exists, this function will get the
|
||||
// properties for the active host context.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_properties_fn)(
|
||||
const hostfxr_handle host_context_handle,
|
||||
/*inout*/ size_t * count,
|
||||
/*out*/ const char_t **keys,
|
||||
/*out*/ const char_t **values);
|
||||
|
||||
//
|
||||
// Load CoreCLR and run the application for an initialized host context
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
//
|
||||
// Return value:
|
||||
// If the app was successfully run, the exit code of the application. Otherwise, the error code result.
|
||||
//
|
||||
// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line.
|
||||
//
|
||||
// This function will not return until the managed application exits.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);
|
||||
|
||||
//
|
||||
// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one.
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
// type
|
||||
// Type of runtime delegate requested
|
||||
// delegate
|
||||
// An out parameter that will be assigned the delegate.
|
||||
//
|
||||
// Return value:
|
||||
// The error code result.
|
||||
//
|
||||
// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config,
|
||||
// then all delegate types are supported.
|
||||
// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line,
|
||||
// then only the following delegate types are currently supported:
|
||||
// hdt_load_assembly_and_get_function_pointer
|
||||
// hdt_get_function_pointer
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)(
|
||||
const hostfxr_handle host_context_handle,
|
||||
enum hostfxr_delegate_type type,
|
||||
/*out*/ void **delegate);
|
||||
|
||||
//
|
||||
// Closes an initialized host context
|
||||
//
|
||||
// Parameters:
|
||||
// host_context_handle
|
||||
// Handle to the initialized host context
|
||||
//
|
||||
// Return value:
|
||||
// The error code result.
|
||||
//
|
||||
typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle);
|
||||
|
||||
struct hostfxr_dotnet_environment_sdk_info
|
||||
{
|
||||
size_t size;
|
||||
const char_t* version;
|
||||
const char_t* path;
|
||||
};
|
||||
|
||||
typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)(
|
||||
const struct hostfxr_dotnet_environment_info* info,
|
||||
void* result_context);
|
||||
|
||||
struct hostfxr_dotnet_environment_framework_info
|
||||
{
|
||||
size_t size;
|
||||
const char_t* name;
|
||||
const char_t* version;
|
||||
const char_t* path;
|
||||
};
|
||||
|
||||
struct hostfxr_dotnet_environment_info
|
||||
{
|
||||
size_t size;
|
||||
|
||||
const char_t* hostfxr_version;
|
||||
const char_t* hostfxr_commit_hash;
|
||||
|
||||
size_t sdk_count;
|
||||
const hostfxr_dotnet_environment_sdk_info* sdks;
|
||||
|
||||
size_t framework_count;
|
||||
const hostfxr_dotnet_environment_framework_info* frameworks;
|
||||
};
|
||||
|
||||
#endif //__HOSTFXR_H__
|
||||
99
Plugins/UnrealSharp/Managed/DotNetRuntime/inc/nethost.h
Normal file
99
Plugins/UnrealSharp/Managed/DotNetRuntime/inc/nethost.h
Normal file
@ -0,0 +1,99 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
#ifndef __NETHOST_H__
|
||||
#define __NETHOST_H__
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef NETHOST_EXPORT
|
||||
#define NETHOST_API __declspec(dllexport)
|
||||
#else
|
||||
// Consuming the nethost as a static library
|
||||
// Shouldn't export attempt to dllimport.
|
||||
#ifdef NETHOST_USE_AS_STATIC
|
||||
#define NETHOST_API
|
||||
#else
|
||||
#define NETHOST_API __declspec(dllimport)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define NETHOST_CALLTYPE __stdcall
|
||||
#ifdef _WCHAR_T_DEFINED
|
||||
typedef wchar_t char_t;
|
||||
#else
|
||||
typedef unsigned short char_t;
|
||||
#endif
|
||||
#else
|
||||
#ifdef NETHOST_EXPORT
|
||||
#define NETHOST_API __attribute__((__visibility__("default")))
|
||||
#else
|
||||
#define NETHOST_API
|
||||
#endif
|
||||
|
||||
#define NETHOST_CALLTYPE
|
||||
typedef char char_t;
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Parameters for get_hostfxr_path
|
||||
//
|
||||
// Fields:
|
||||
// size
|
||||
// Size of the struct. This is used for versioning.
|
||||
//
|
||||
// assembly_path
|
||||
// Path to the compenent's assembly.
|
||||
// If specified, hostfxr is located as if the assembly_path is the apphost
|
||||
//
|
||||
// dotnet_root
|
||||
// Path to directory containing the dotnet executable.
|
||||
// If specified, hostfxr is located as if an application is started using
|
||||
// 'dotnet app.dll', which means it will be searched for under the dotnet_root
|
||||
// path and the assembly_path is ignored.
|
||||
//
|
||||
struct get_hostfxr_parameters {
|
||||
size_t size;
|
||||
const char_t *assembly_path;
|
||||
const char_t *dotnet_root;
|
||||
};
|
||||
|
||||
//
|
||||
// Get the path to the hostfxr library
|
||||
//
|
||||
// Parameters:
|
||||
// buffer
|
||||
// Buffer that will be populated with the hostfxr path, including a null terminator.
|
||||
//
|
||||
// buffer_size
|
||||
// [in] Size of buffer in char_t units.
|
||||
// [out] Size of buffer used in char_t units. If the input value is too small
|
||||
// or buffer is nullptr, this is populated with the minimum required size
|
||||
// in char_t units for a buffer to hold the hostfxr path
|
||||
//
|
||||
// get_hostfxr_parameters
|
||||
// Optional. Parameters that modify the behaviour for locating the hostfxr library.
|
||||
// If nullptr, hostfxr is located using the enviroment variable or global registration
|
||||
//
|
||||
// Return value:
|
||||
// 0 on success, otherwise failure
|
||||
// 0x80008098 - buffer is too small (HostApiBufferTooSmall)
|
||||
//
|
||||
// Remarks:
|
||||
// The full search for the hostfxr library is done on every call. To minimize the need
|
||||
// to call this function multiple times, pass a large buffer (e.g. PATH_MAX).
|
||||
//
|
||||
NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path(
|
||||
char_t * buffer,
|
||||
size_t * buffer_size,
|
||||
const struct get_hostfxr_parameters *parameters);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif // __NETHOST_H__
|
||||
252
Plugins/UnrealSharp/Managed/Shared/DotNetUtilities.cs
Normal file
252
Plugins/UnrealSharp/Managed/Shared/DotNetUtilities.cs
Normal file
@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace UnrealSharp.Shared;
|
||||
|
||||
public static class DotNetUtilities
|
||||
{
|
||||
public const string DOTNET_MAJOR_VERSION = "9.0";
|
||||
public const string DOTNET_MAJOR_VERSION_DISPLAY = "net" + DOTNET_MAJOR_VERSION;
|
||||
|
||||
public static string FindDotNetExecutable()
|
||||
{
|
||||
const string DOTNET_WIN = "dotnet.exe";
|
||||
const string DOTNET_UNIX = "dotnet";
|
||||
|
||||
var dotnetExe = OperatingSystem.IsWindows() ? DOTNET_WIN : DOTNET_UNIX;
|
||||
|
||||
var pathVariable = Environment.GetEnvironmentVariable("PATH");
|
||||
|
||||
if (pathVariable == null)
|
||||
{
|
||||
throw new Exception($"Couldn't find {dotnetExe}!");
|
||||
}
|
||||
|
||||
var paths = pathVariable.Split(Path.PathSeparator);
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
// This is a hack to avoid using the dotnet.exe from the Unreal Engine installation directory.
|
||||
// Can't use the dotnet.exe from the Unreal Engine installation directory because it's .NET 6.0
|
||||
if (!path.Contains(@"\dotnet\"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dotnetExePath = Path.Combine(path, dotnetExe);
|
||||
|
||||
if (File.Exists(dotnetExePath))
|
||||
{
|
||||
return dotnetExePath;
|
||||
}
|
||||
}
|
||||
|
||||
if ( OperatingSystem.IsMacOS() ) {
|
||||
if ( File.Exists( "/usr/local/share/dotnet/dotnet" ) ) {
|
||||
return "/usr/local/share/dotnet/dotnet";
|
||||
}
|
||||
if ( File.Exists( "/opt/homebrew/bin/dotnet" ) ) {
|
||||
return "/opt/homebrew/bin/dotnet";
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Couldn't find {dotnetExe} in PATH!");
|
||||
}
|
||||
|
||||
public static string GetLatestDotNetSdkPath()
|
||||
{
|
||||
string dotNetExecutable = FindDotNetExecutable();
|
||||
string dotNetExecutableDirectory = Path.GetDirectoryName(dotNetExecutable)!;
|
||||
string dotNetSdkDirectory = Path.Combine(dotNetExecutableDirectory!, "sdk");
|
||||
|
||||
string[] folderPaths = Directory.GetDirectories(dotNetSdkDirectory);
|
||||
|
||||
string highestVersion = "0.0.0";
|
||||
|
||||
foreach (string folderPath in folderPaths)
|
||||
{
|
||||
string folderName = Path.GetFileName(folderPath);
|
||||
|
||||
if (string.IsNullOrEmpty(folderName) || !char.IsDigit(folderName[0]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Compare(folderName, highestVersion, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
highestVersion = folderName;
|
||||
}
|
||||
}
|
||||
|
||||
if (highestVersion == "0.0.0")
|
||||
{
|
||||
throw new Exception("Failed to find the latest .NET SDK version.");
|
||||
}
|
||||
|
||||
if (!highestVersion.StartsWith(DOTNET_MAJOR_VERSION))
|
||||
{
|
||||
throw new Exception($"Failed to find the latest .NET SDK version. Expected version to start with {DOTNET_MAJOR_VERSION} but found: {highestVersion}");
|
||||
}
|
||||
|
||||
return Path.Combine(dotNetSdkDirectory, highestVersion);
|
||||
}
|
||||
|
||||
public static void BuildSolution(string projectRootDirectory, string managedBinariesPath)
|
||||
{
|
||||
if (!Directory.Exists(projectRootDirectory))
|
||||
{
|
||||
throw new Exception($"Couldn't find project root directory: {projectRootDirectory}");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(managedBinariesPath))
|
||||
{
|
||||
Directory.CreateDirectory(managedBinariesPath);
|
||||
}
|
||||
|
||||
Collection<string> arguments = new Collection<string>
|
||||
{
|
||||
"publish",
|
||||
$"-p:PublishDir=\"{managedBinariesPath}\""
|
||||
};
|
||||
|
||||
InvokeDotNet(arguments, projectRootDirectory);
|
||||
}
|
||||
|
||||
public static bool InvokeDotNet(Collection<string> arguments, string? workingDirectory = null)
|
||||
{
|
||||
string dotnetPath = FindDotNetExecutable();
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = dotnetPath,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
foreach (string argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
if (workingDirectory != null)
|
||||
{
|
||||
startInfo.WorkingDirectory = workingDirectory;
|
||||
}
|
||||
|
||||
// Set the MSBuild environment variables to the latest .NET SDK that U# supports.
|
||||
// Otherwise, we'll use the .NET SDK that comes with the Unreal Engine.
|
||||
{
|
||||
string latestDotNetSdkPath = GetLatestDotNetSdkPath();
|
||||
startInfo.Environment["MSBuildExtensionsPath"] = latestDotNetSdkPath;
|
||||
startInfo.Environment["MSBUILD_EXE_PATH"] = $@"{latestDotNetSdkPath}\MSBuild.dll";
|
||||
startInfo.Environment["MSBuildSDKsPath"] = $@"{latestDotNetSdkPath}\Sdks";
|
||||
}
|
||||
|
||||
using Process process = new Process();
|
||||
process.StartInfo = startInfo;
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder outputBuilder = new StringBuilder();
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
throw new Exception("Failed to start process");
|
||||
}
|
||||
|
||||
process.BeginErrorReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
process.WaitForExit();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
string errorMessage = outputBuilder.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
errorMessage = "Process exited with non-zero exit code but no output was captured.";
|
||||
}
|
||||
|
||||
throw new Exception($"Process failed with exit code {process.ExitCode}: {errorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"An error occurred: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool InvokeUSharpBuildTool(string action,
|
||||
string managedBinariesPath,
|
||||
string projectName,
|
||||
string pluginDirectory,
|
||||
string projectDirectory,
|
||||
string engineDirectory,
|
||||
IEnumerable<KeyValuePair<string, string>>? additionalArguments = null)
|
||||
{
|
||||
string dotNetExe = FindDotNetExecutable();
|
||||
string unrealSharpBuildToolPath = Path.Combine(managedBinariesPath, "UnrealSharpBuildTool.dll");
|
||||
|
||||
if (!File.Exists(unrealSharpBuildToolPath))
|
||||
{
|
||||
throw new Exception($"Failed to find UnrealSharpBuildTool.dll at: {unrealSharpBuildToolPath}");
|
||||
}
|
||||
|
||||
Collection<string> arguments = new Collection<string>
|
||||
{
|
||||
unrealSharpBuildToolPath,
|
||||
|
||||
"--Action",
|
||||
action,
|
||||
|
||||
"--EngineDirectory",
|
||||
$"{engineDirectory}",
|
||||
|
||||
"--ProjectDirectory",
|
||||
$"{projectDirectory}",
|
||||
|
||||
"--ProjectName",
|
||||
projectName,
|
||||
|
||||
"--PluginDirectory",
|
||||
$"{pluginDirectory}",
|
||||
|
||||
"--DotNetPath",
|
||||
$"{dotNetExe}"
|
||||
};
|
||||
|
||||
if (additionalArguments != null)
|
||||
{
|
||||
arguments.Add("--AdditionalArgs");
|
||||
|
||||
foreach (KeyValuePair<string, string> argument in additionalArguments)
|
||||
{
|
||||
arguments.Add($"{argument.Key}={argument.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
return InvokeDotNet(arguments);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
namespace UnrealSharp.Binds;
|
||||
|
||||
public static class NativeBinds
|
||||
{
|
||||
private unsafe static delegate* unmanaged[Cdecl]<char*, char*, int, IntPtr> _getBoundFunction = null;
|
||||
|
||||
public unsafe static void InitializeNativeBinds(IntPtr bindsCallbacks)
|
||||
{
|
||||
if (_getBoundFunction != null)
|
||||
{
|
||||
throw new Exception("NativeBinds.InitializeNativeBinds called twice");
|
||||
}
|
||||
|
||||
_getBoundFunction = (delegate* unmanaged[Cdecl]<char*, char*, int, IntPtr>)bindsCallbacks;
|
||||
}
|
||||
|
||||
public unsafe static IntPtr TryGetBoundFunction(string outerName, string functionName, int functionSize)
|
||||
{
|
||||
if (_getBoundFunction == null)
|
||||
{
|
||||
throw new Exception("NativeBinds not initialized");
|
||||
}
|
||||
|
||||
IntPtr functionPtr = IntPtr.Zero;
|
||||
fixed (char* outerNamePtr = outerName)
|
||||
fixed (char* functionNamePtr = functionName)
|
||||
{
|
||||
functionPtr = _getBoundFunction(outerNamePtr, functionNamePtr, functionSize);
|
||||
}
|
||||
|
||||
if (functionPtr == IntPtr.Zero)
|
||||
{
|
||||
throw new Exception($"Failed to find bound function {functionName} in {outerName}");
|
||||
}
|
||||
|
||||
return functionPtr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace UnrealSharp.Binds;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
public class NativeCallbacksAttribute : Attribute;
|
||||
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@ -0,0 +1,3 @@
|
||||
namespace UnrealSharp.Core.Attributes;
|
||||
|
||||
public class BindingAttribute : Attribute;
|
||||
@ -0,0 +1,4 @@
|
||||
namespace UnrealSharp.Core.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public class BlittableTypeAttribute : Attribute;
|
||||
@ -0,0 +1,17 @@
|
||||
namespace UnrealSharp.Core.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Used to mark a type as generated. Don't use this attribute in your code.
|
||||
/// It's public since glue for user code is generated in the user's project.
|
||||
/// </summary>
|
||||
public class GeneratedTypeAttribute : Attribute
|
||||
{
|
||||
public GeneratedTypeAttribute(string engineName, string fullName = "")
|
||||
{
|
||||
EngineName = engineName;
|
||||
FullName = fullName;
|
||||
}
|
||||
|
||||
public string EngineName;
|
||||
public string FullName;
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Struct representing a handle to a delegate.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FDelegateHandle : IEquatable<FDelegateHandle>
|
||||
{
|
||||
public ulong ID;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ID = 0;
|
||||
}
|
||||
|
||||
public static bool operator ==(FDelegateHandle a, FDelegateHandle b)
|
||||
{
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(FDelegateHandle a, FDelegateHandle b)
|
||||
{
|
||||
return !a.Equals(b);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is FDelegateHandle handle && Equals(handle);
|
||||
}
|
||||
|
||||
public bool Equals(FDelegateHandle other)
|
||||
{
|
||||
return ID == other.ID;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ID.GetHashCode();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using UnrealSharp.Binds;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FCSManagerExporter
|
||||
{
|
||||
public static delegate* unmanaged<IntPtr, IntPtr> FindManagedObject;
|
||||
public static delegate* unmanaged<IntPtr, IntPtr, IntPtr> FindOrCreateManagedInterfaceWrapper;
|
||||
public static delegate* unmanaged<IntPtr> GetCurrentWorldContext;
|
||||
public static delegate* unmanaged<IntPtr> GetCurrentWorldPtr;
|
||||
|
||||
public static UnrealSharpObject WorldContextObject
|
||||
{
|
||||
get
|
||||
{
|
||||
IntPtr worldContextObject = CallGetCurrentWorldContext();
|
||||
IntPtr handle = CallFindManagedObject(worldContextObject);
|
||||
return GCHandleUtilities.GetObjectFromHandlePtr<UnrealSharpObject>(handle)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
public static class GCHandleUtilities
|
||||
{
|
||||
private static readonly ConcurrentDictionary<AssemblyLoadContext, ConcurrentDictionary<GCHandle, object>> StrongRefsByAssembly = new();
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void OnAlcUnloading(AssemblyLoadContext alc)
|
||||
{
|
||||
StrongRefsByAssembly.TryRemove(alc, out _);
|
||||
}
|
||||
|
||||
public static GCHandle AllocateStrongPointer(object value, object allocator)
|
||||
{
|
||||
return AllocateStrongPointer(value, allocator.GetType().Assembly);
|
||||
}
|
||||
|
||||
public static GCHandle AllocateStrongPointer(object value, Assembly alc)
|
||||
{
|
||||
AssemblyLoadContext? assemblyLoadContext = AssemblyLoadContext.GetLoadContext(alc);
|
||||
|
||||
if (assemblyLoadContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("AssemblyLoadContext is null.");
|
||||
}
|
||||
|
||||
return AllocateStrongPointer(value, assemblyLoadContext);
|
||||
}
|
||||
|
||||
public static GCHandle AllocateStrongPointer(object value, AssemblyLoadContext loadContext)
|
||||
{
|
||||
GCHandle weakHandle = GCHandle.Alloc(value, GCHandleType.Weak);
|
||||
ConcurrentDictionary<GCHandle, object> strongReferences = StrongRefsByAssembly.GetOrAdd(loadContext, alcInstance =>
|
||||
{
|
||||
alcInstance.Unloading += OnAlcUnloading;
|
||||
return new ConcurrentDictionary<GCHandle, object>();
|
||||
});
|
||||
|
||||
strongReferences.TryAdd(weakHandle, value);
|
||||
|
||||
return weakHandle;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GCHandle AllocateWeakPointer(object value) => GCHandle.Alloc(value, GCHandleType.Weak);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GCHandle AllocatePinnedPointer(object value) => GCHandle.Alloc(value, GCHandleType.Pinned);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void Free(GCHandle handle, Assembly? assembly)
|
||||
{
|
||||
if (assembly != null)
|
||||
{
|
||||
AssemblyLoadContext? assemblyLoadContext = AssemblyLoadContext.GetLoadContext(assembly);
|
||||
|
||||
if (assemblyLoadContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("AssemblyLoadContext is null.");
|
||||
}
|
||||
|
||||
if (StrongRefsByAssembly.TryGetValue(assemblyLoadContext, out ConcurrentDictionary<GCHandle, object>? strongReferences))
|
||||
{
|
||||
strongReferences.TryRemove(handle, out _);
|
||||
}
|
||||
}
|
||||
|
||||
handle.Free();
|
||||
}
|
||||
|
||||
public static T? GetObjectFromHandlePtr<T>(IntPtr handle)
|
||||
{
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
GCHandle subObjectGcHandle = GCHandle.FromIntPtr(handle);
|
||||
if (!subObjectGcHandle.IsAllocated)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
object? subObject = subObjectGcHandle.Target;
|
||||
if (subObject is T typedObject)
|
||||
{
|
||||
return typedObject;
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using UnrealSharp.Binds;
|
||||
|
||||
namespace UnrealSharp.Core.Interop;
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FScriptArrayExporter
|
||||
{
|
||||
public static delegate* unmanaged<UnmanagedArray*, IntPtr> GetData;
|
||||
public static delegate* unmanaged<UnmanagedArray*, int, NativeBool> IsValidIndex;
|
||||
public static delegate* unmanaged<UnmanagedArray*, int> Num;
|
||||
public static delegate* unmanaged<UnmanagedArray*, void> Destroy;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using UnrealSharp.Binds;
|
||||
|
||||
namespace UnrealSharp.Interop;
|
||||
|
||||
[NativeCallbacks]
|
||||
public unsafe partial class FStringExporter
|
||||
{
|
||||
public static delegate* unmanaged<IntPtr, char*, void> MarshalToNativeString;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using UnrealSharp.Binds;
|
||||
|
||||
namespace UnrealSharp.Core.Interop;
|
||||
|
||||
[NativeCallbacks]
|
||||
public unsafe partial class ManagedHandleExporter
|
||||
{
|
||||
public static delegate* unmanaged<IntPtr, IntPtr, void> StoreManagedHandle;
|
||||
public static delegate* unmanaged<IntPtr, IntPtr> LoadManagedHandle;
|
||||
public static delegate* unmanaged<IntPtr, IntPtr, int, void> StoreUnmanagedMemory;
|
||||
public static delegate* unmanaged<IntPtr, IntPtr, int, void> LoadUnmanagedMemory;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
using UnrealSharp.Log;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
[CustomLog]
|
||||
public static partial class LogUnrealSharpCore;
|
||||
@ -0,0 +1,31 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct ManagedCallbacks
|
||||
{
|
||||
public delegate* unmanaged<IntPtr, IntPtr, char**, IntPtr> ScriptManagerBridge_CreateManagedObject;
|
||||
public delegate* unmanaged<IntPtr, IntPtr, IntPtr> ScriptManagerBridge_CreateNewManagedObjectWrapper;
|
||||
public delegate* unmanaged<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, int> ScriptManagerBridge_InvokeManagedMethod;
|
||||
public delegate* unmanaged<IntPtr, void> ScriptManagerBridge_InvokeDelegate;
|
||||
public delegate* unmanaged<IntPtr, char*, IntPtr> ScriptManagerBridge_LookupManagedMethod;
|
||||
public delegate* unmanaged<IntPtr, char*, IntPtr> ScriptManagedBridge_LookupManagedType;
|
||||
public delegate* unmanaged<IntPtr, IntPtr, void> ScriptManagedBridge_Dispose;
|
||||
public delegate* unmanaged<IntPtr, void> ScriptManagedBridge_FreeHandle;
|
||||
|
||||
public static void Initialize(IntPtr outManagedCallbacks)
|
||||
{
|
||||
*(ManagedCallbacks*)outManagedCallbacks = new ManagedCallbacks
|
||||
{
|
||||
ScriptManagerBridge_CreateManagedObject = &UnmanagedCallbacks.CreateNewManagedObject,
|
||||
ScriptManagerBridge_CreateNewManagedObjectWrapper = &UnmanagedCallbacks.CreateNewManagedObjectWrapper,
|
||||
ScriptManagerBridge_InvokeManagedMethod = &UnmanagedCallbacks.InvokeManagedMethod,
|
||||
ScriptManagerBridge_InvokeDelegate = &UnmanagedCallbacks.InvokeDelegate,
|
||||
ScriptManagerBridge_LookupManagedMethod = &UnmanagedCallbacks.LookupManagedMethod,
|
||||
ScriptManagedBridge_LookupManagedType = &UnmanagedCallbacks.LookupManagedType,
|
||||
ScriptManagedBridge_Dispose = &UnmanagedCallbacks.Dispose,
|
||||
ScriptManagedBridge_FreeHandle = &UnmanagedCallbacks.FreeHandle,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class BitfieldBoolMarshaller
|
||||
{
|
||||
private const int BoolSize = sizeof(NativeBool);
|
||||
|
||||
public static void ToNative(IntPtr valuePtr, byte fieldMask, bool value)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var byteValue = (byte*)valuePtr;
|
||||
var mask = value ? fieldMask : byte.MinValue;
|
||||
*byteValue = (byte)((*byteValue & ~fieldMask) | mask);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool FromNative(IntPtr valuePtr, byte fieldMask)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var byteValue = (byte*)valuePtr;
|
||||
return (*byteValue & fieldMask) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class BlittableMarshaller<T> where T : unmanaged, allows ref struct
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
ToNative(nativeBuffer, arrayIndex, obj, sizeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj, int size)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
*(T*)(nativeBuffer + arrayIndex * size) = obj;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return FromNative(nativeBuffer, arrayIndex, sizeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T FromNative(IntPtr nativeBuffer, int arrayIndex, int size)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return *(T*)(nativeBuffer + arrayIndex * size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class BoolMarshaller
|
||||
{
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, bool obj)
|
||||
{
|
||||
BlittableMarshaller<NativeBool>.ToNative(nativeBuffer, arrayIndex, obj.ToNativeBool());
|
||||
}
|
||||
|
||||
public static bool FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
return BlittableMarshaller<NativeBool>.FromNative(nativeBuffer, arrayIndex).ToManagedBool();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class EnumMarshaller<T> where T : Enum
|
||||
{
|
||||
public static T FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
byte value = BlittableMarshaller<byte>.FromNative(nativeBuffer, arrayIndex);
|
||||
return (T) Enum.ToObject(typeof(T), value);
|
||||
}
|
||||
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj)
|
||||
{
|
||||
byte value = Convert.ToByte(obj);
|
||||
BlittableMarshaller<byte>.ToNative(nativeBuffer, arrayIndex, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
|
||||
namespace UnrealSharp;
|
||||
|
||||
public interface MarshalledStruct<Self> where Self : MarshalledStruct<Self>, allows ref struct
|
||||
{
|
||||
public static abstract IntPtr GetNativeClassPtr();
|
||||
|
||||
public static abstract int GetNativeDataSize();
|
||||
|
||||
public static abstract Self FromNative(IntPtr buffer);
|
||||
|
||||
public void ToNative(IntPtr buffer);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class MarshallingDelegates<T>
|
||||
{
|
||||
public delegate void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj);
|
||||
public delegate T FromNative(IntPtr nativeBuffer, int arrayIndex);
|
||||
public delegate void DestructInstance(IntPtr nativeBuffer, int arrayIndex);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class ObjectMarshaller<T> where T : UnrealSharpObject
|
||||
{
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj)
|
||||
{
|
||||
IntPtr uObjectPosition = nativeBuffer + arrayIndex * IntPtr.Size;
|
||||
|
||||
unsafe
|
||||
{
|
||||
*(IntPtr*)uObjectPosition = obj?.NativeObject ?? IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public static T FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
IntPtr uObjectPointer = BlittableMarshaller<IntPtr>.FromNative(nativeBuffer, arrayIndex);
|
||||
|
||||
if (uObjectPointer == IntPtr.Zero)
|
||||
{
|
||||
return null!;
|
||||
}
|
||||
|
||||
IntPtr handle = FCSManagerExporter.CallFindManagedObject(uObjectPointer);
|
||||
return GCHandleUtilities.GetObjectFromHandlePtr<T>(handle)!;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using UnrealSharp.Interop;
|
||||
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class StringMarshaller
|
||||
{
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, string obj)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (string.IsNullOrEmpty(obj))
|
||||
{
|
||||
//Guard against C# null strings (use string.Empty instead)
|
||||
obj = string.Empty;
|
||||
}
|
||||
|
||||
IntPtr unrealString = nativeBuffer + arrayIndex * sizeof(UnmanagedArray);
|
||||
|
||||
fixed (char* stringPtr = obj)
|
||||
{
|
||||
FStringExporter.CallMarshalToNativeString(unrealString, stringPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UnmanagedArray unrealString = BlittableMarshaller<UnmanagedArray>.FromNative(nativeBuffer, arrayIndex);
|
||||
return unrealString.Data == IntPtr.Zero ? string.Empty : new string((char*) unrealString.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DestructInstance(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UnmanagedArray* unrealString = (UnmanagedArray*) (nativeBuffer + arrayIndex * sizeof(UnmanagedArray));
|
||||
unrealString->Destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace UnrealSharp.Core.Marshallers;
|
||||
|
||||
public static class StructMarshaller<T> where T : MarshalledStruct<T>
|
||||
{
|
||||
public static T FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
return T.FromNative(nativeBuffer + arrayIndex * T.GetNativeDataSize());
|
||||
}
|
||||
|
||||
public static void ToNative(IntPtr nativeBuffer, int arrayIndex, T obj)
|
||||
{
|
||||
obj.ToNative(nativeBuffer + arrayIndex * T.GetNativeDataSize());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace UnrealSharp.Engine.Core.Modules;
|
||||
|
||||
public interface IModuleInterface
|
||||
{
|
||||
public void StartupModule();
|
||||
public void ShutdownModule();
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
// Bools are not blittable, so we need to convert them to bytes
|
||||
public enum NativeBool : byte
|
||||
{
|
||||
False = 0,
|
||||
True = 1
|
||||
}
|
||||
|
||||
public static class BoolConverter
|
||||
{
|
||||
public static NativeBool ToNativeBool(this bool value)
|
||||
{
|
||||
return value ? NativeBool.True : NativeBool.False;
|
||||
}
|
||||
|
||||
public static bool ToManagedBool(this NativeBool value)
|
||||
{
|
||||
byte byteValue = (byte) value;
|
||||
return byteValue != 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnrealSharp.Core.Interop;
|
||||
using UnrealSharp.Core.Marshallers;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct UnmanagedArray
|
||||
{
|
||||
public IntPtr Data;
|
||||
public int ArrayNum;
|
||||
public int ArrayMax;
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (UnmanagedArray* ptr = &this)
|
||||
{
|
||||
FScriptArrayExporter.CallDestroy(ptr);
|
||||
}
|
||||
|
||||
Data = IntPtr.Zero;
|
||||
ArrayNum = 0;
|
||||
ArrayMax = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public List<T> ToBlittableList<T>() where T : unmanaged
|
||||
{
|
||||
List<T> list = new List<T>(ArrayNum);
|
||||
|
||||
unsafe
|
||||
{
|
||||
T* data = (T*) Data.ToPointer();
|
||||
for (int i = 0; i < ArrayNum; i++)
|
||||
{
|
||||
list.Add(data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<T> ToListWithMarshaller<T>(Func<IntPtr, int, T> resolver)
|
||||
{
|
||||
List<T> list = new List<T>(ArrayNum);
|
||||
|
||||
for (int i = 0; i < ArrayNum; i++)
|
||||
{
|
||||
list.Add(resolver(Data, i));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void ForEachWithMarshaller<T>(Func<IntPtr, int, T> resolver, Action<T> action)
|
||||
{
|
||||
for (int i = 0; i < ArrayNum; i++)
|
||||
{
|
||||
T item = resolver(Data, i);
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,254 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnrealSharp.Core.Attributes;
|
||||
using UnrealSharp.Core.Marshallers;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
public static class UnmanagedCallbacks
|
||||
{
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe IntPtr CreateNewManagedObject(IntPtr nativeObject, IntPtr typeHandlePtr, char** error)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (nativeObject == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nativeObject));
|
||||
}
|
||||
|
||||
Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided type handle does not point to a valid type.");
|
||||
}
|
||||
|
||||
return UnrealSharpObject.Create(type, nativeObject);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogUnrealSharpCore.LogError($"Failed to create new managed object: {ex.Message}");
|
||||
*error = (char*)Marshal.StringToHGlobalUni(ex.ToString());
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static IntPtr CreateNewManagedObjectWrapper(IntPtr managedObjectHandle, IntPtr typeHandlePtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (managedObjectHandle == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(managedObjectHandle));
|
||||
}
|
||||
|
||||
Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided type handle does not point to a valid type.");
|
||||
}
|
||||
|
||||
object? managedObject = GCHandleUtilities.GetObjectFromHandlePtr<object>(managedObjectHandle);
|
||||
if (managedObject is null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided managed object handle does not point to a valid object.");
|
||||
}
|
||||
|
||||
MethodInfo? wrapMethod = type.GetMethod("Wrap", BindingFlags.Public | BindingFlags.Static);
|
||||
if (wrapMethod is null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided type does not have a static Wrap method.");
|
||||
}
|
||||
|
||||
object? createdObject = wrapMethod.Invoke(null, [managedObject]);
|
||||
if (createdObject is null)
|
||||
{
|
||||
throw new InvalidOperationException("The Wrap method did not return a valid object.");
|
||||
}
|
||||
|
||||
return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(createdObject, createdObject.GetType().Assembly));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogUnrealSharpCore.LogError($"Failed to create new managed object: {ex.Message}");
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe IntPtr LookupManagedMethod(IntPtr typeHandlePtr, char* methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Type? type = GCHandleUtilities.GetObjectFromHandlePtr<Type>(typeHandlePtr);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new Exception("Invalid type handle");
|
||||
}
|
||||
|
||||
string methodNameString = new string(methodName);
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
Type? currentType = type;
|
||||
|
||||
while (currentType != null)
|
||||
{
|
||||
MethodInfo? method = currentType.GetMethod(methodNameString, flags);
|
||||
|
||||
if (method != null)
|
||||
{
|
||||
IntPtr functionPtr = method.MethodHandle.GetFunctionPointer();
|
||||
GCHandle methodHandle = GCHandleUtilities.AllocateStrongPointer(functionPtr, type.Assembly);
|
||||
return GCHandle.ToIntPtr(methodHandle);
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogUnrealSharpCore.LogError($"Exception while trying to look up managed method: {e.Message}");
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe IntPtr LookupManagedType(IntPtr assemblyHandle, char* fullTypeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fullTypeNameString = new string(fullTypeName);
|
||||
Assembly? loadedAssembly = GCHandleUtilities.GetObjectFromHandlePtr<Assembly>(assemblyHandle);
|
||||
|
||||
if (loadedAssembly == null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided assembly handle does not point to a valid assembly.");
|
||||
}
|
||||
|
||||
return FindTypeInAssembly(loadedAssembly, fullTypeNameString);
|
||||
}
|
||||
catch (TypeLoadException ex)
|
||||
{
|
||||
LogUnrealSharpCore.LogError($"TypeLoadException while trying to look up managed type: {ex.Message}");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr FindTypeInAssembly(Assembly assembly, string fullTypeName)
|
||||
{
|
||||
Type[] types = assembly.GetTypes();
|
||||
foreach (Type type in types)
|
||||
{
|
||||
foreach (CustomAttributeData attributeData in type.CustomAttributes)
|
||||
{
|
||||
if (attributeData.AttributeType.FullName != typeof(GeneratedTypeAttribute).FullName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attributeData.ConstructorArguments.Count != 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string fullName = (string)attributeData.ConstructorArguments[1].Value!;
|
||||
if (fullName == fullTypeName)
|
||||
{
|
||||
return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(type, assembly));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe int InvokeManagedMethod(IntPtr managedObjectHandle,
|
||||
IntPtr methodHandlePtr,
|
||||
IntPtr argumentsBuffer,
|
||||
IntPtr returnValueBuffer,
|
||||
IntPtr exceptionTextBuffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr? methodHandle = GCHandleUtilities.GetObjectFromHandlePtr<IntPtr>(methodHandlePtr);
|
||||
object? managedObject = GCHandleUtilities.GetObjectFromHandlePtr<object>(managedObjectHandle);
|
||||
|
||||
if (methodHandle == null || managedObject == null)
|
||||
{
|
||||
throw new Exception("Invalid method or target handle");
|
||||
}
|
||||
|
||||
delegate*<object, IntPtr, IntPtr, void> methodPtr = (delegate*<object, IntPtr, IntPtr, void>) methodHandle;
|
||||
methodPtr(managedObject, argumentsBuffer, returnValueBuffer);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StringMarshaller.ToNative(exceptionTextBuffer, 0, ex.ToString());
|
||||
LogUnrealSharpCore.LogError($"Exception during InvokeManagedMethod: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static void InvokeDelegate(IntPtr delegatePtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
Delegate? foundDelegate = GCHandleUtilities.GetObjectFromHandlePtr<Delegate>(delegatePtr);
|
||||
|
||||
if (foundDelegate == null)
|
||||
{
|
||||
throw new Exception("Invalid delegate handle");
|
||||
}
|
||||
|
||||
foundDelegate.DynamicInvoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogUnrealSharpCore.LogError($"Exception during InvokeDelegate: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static void Dispose(IntPtr handle, IntPtr assemblyHandle)
|
||||
{
|
||||
GCHandle foundHandle = GCHandle.FromIntPtr(handle);
|
||||
|
||||
if (!foundHandle.IsAllocated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundHandle.Target is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
Assembly? foundAssembly = GCHandleUtilities.GetObjectFromHandlePtr<Assembly>(assemblyHandle);
|
||||
GCHandleUtilities.Free(foundHandle, foundAssembly);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static void FreeHandle(IntPtr handle)
|
||||
{
|
||||
GCHandle foundHandle = GCHandle.FromIntPtr(handle);
|
||||
if (!foundHandle.IsAllocated) return;
|
||||
|
||||
if (foundHandle.Target is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
foundHandle.Free();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UnrealSharp.Binds\UnrealSharp.Binds.csproj" />
|
||||
<ProjectReference Include="..\UnrealSharp.Log\UnrealSharp.Log.csproj" />
|
||||
|
||||
<ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj">
|
||||
<OutputItemType>Analyzer</OutputItemType>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,44 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnrealSharp.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a UObject in Unreal Engine. Don't inherit from this class directly, use a CoreUObject.Object instead.
|
||||
/// </summary>
|
||||
public class UnrealSharpObject : IDisposable
|
||||
{
|
||||
internal static unsafe IntPtr Create(Type typeToCreate, IntPtr nativeObjectPtr)
|
||||
{
|
||||
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
ConstructorInfo? foundDefaultCtor = typeToCreate.GetConstructor(bindingFlags, Type.EmptyTypes);
|
||||
|
||||
if (foundDefaultCtor == null)
|
||||
{
|
||||
LogUnrealSharpCore.LogError("Failed to find default constructor for type: " + typeToCreate.FullName);
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
delegate*<object, void> foundConstructor = (delegate*<object, void>) foundDefaultCtor.MethodHandle.GetFunctionPointer();
|
||||
|
||||
UnrealSharpObject createdObject = (UnrealSharpObject) RuntimeHelpers.GetUninitializedObject(typeToCreate);
|
||||
createdObject.NativeObject = nativeObjectPtr;
|
||||
|
||||
foundConstructor(createdObject);
|
||||
|
||||
return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(createdObject, typeToCreate.Assembly));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The pointer to the UObject that this C# object represents.
|
||||
/// </summary>
|
||||
public IntPtr NativeObject { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Dispose()
|
||||
{
|
||||
NativeObject = IntPtr.Zero;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
using UnrealSharp.Binds;
|
||||
using UnrealSharp.Core;
|
||||
|
||||
namespace UnrealSharp.Editor.Interop;
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FUnrealSharpEditorModuleExporter
|
||||
{
|
||||
public static delegate* unmanaged<FManagedUnrealSharpEditorCallbacks, void> InitializeUnrealSharpEditorCallbacks;
|
||||
public static delegate* unmanaged<out UnmanagedArray, void> GetProjectPaths;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
using UnrealSharp.Log;
|
||||
|
||||
namespace UnrealSharp.Editor;
|
||||
|
||||
[CustomLog]
|
||||
public static partial class LogUnrealSharpEditor;
|
||||
@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssembliesName>true</CopyLocalLockFileAssembliesName>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DefineConstants>WITH_EDITOR</DefineConstants>
|
||||
<DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants>
|
||||
<DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UnrealSharp.Plugins\UnrealSharp.Plugins.csproj" />
|
||||
<ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj">
|
||||
<OutputItemType>Analyzer</OutputItemType>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\UnrealSharp\UnrealSharp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="UnrealSharpWeaver">
|
||||
<HintPath>..\..\..\Binaries\Managed\UnrealSharpWeaver.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,230 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Build.Evaluation;
|
||||
using Microsoft.Build.Execution;
|
||||
using Microsoft.Build.Framework;
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.Core.Marshallers;
|
||||
using UnrealSharp.Editor.Interop;
|
||||
using UnrealSharp.Engine.Core.Modules;
|
||||
using UnrealSharp.Shared;
|
||||
using UnrealSharpWeaver;
|
||||
|
||||
namespace UnrealSharp.Editor;
|
||||
|
||||
// TODO: Automate managed callbacks so we easily can make calls from native to managed.
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct FManagedUnrealSharpEditorCallbacks
|
||||
{
|
||||
public delegate* unmanaged<char*, char*, char*, LoggerVerbosity, IntPtr, NativeBool, NativeBool> BuildProjects;
|
||||
public delegate* unmanaged<void> ForceManagedGC;
|
||||
public delegate* unmanaged<char*, IntPtr, NativeBool> OpenSolution;
|
||||
public delegate* unmanaged<char*, void> AddProjectToCollection;
|
||||
|
||||
public FManagedUnrealSharpEditorCallbacks()
|
||||
{
|
||||
BuildProjects = &ManagedUnrealSharpEditorCallbacks.Build;
|
||||
ForceManagedGC = &ManagedUnrealSharpEditorCallbacks.ForceManagedGC;
|
||||
OpenSolution = &ManagedUnrealSharpEditorCallbacks.OpenSolution;
|
||||
AddProjectToCollection = &ManagedUnrealSharpEditorCallbacks.AddProjectToCollection;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorCollectingLogger : ILogger
|
||||
{
|
||||
public StringBuilder ErrorLog { get; } = new();
|
||||
public LoggerVerbosity Verbosity { get; set; }
|
||||
public string Parameters { get; set; } = string.Empty;
|
||||
|
||||
public ErrorCollectingLogger(LoggerVerbosity verbosity = LoggerVerbosity.Normal)
|
||||
{
|
||||
Verbosity = verbosity;
|
||||
}
|
||||
|
||||
public void Initialize(IEventSource eventSource)
|
||||
{
|
||||
eventSource.ErrorRaised += (sender, e) =>
|
||||
{
|
||||
string fileName = Path.GetFileName(e.File);
|
||||
|
||||
ErrorLog.AppendLine($"{fileName}({e.LineNumber},{e.ColumnNumber}): {e.Message}");
|
||||
ErrorLog.AppendLine();
|
||||
};
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class ManagedUnrealSharpEditorCallbacks
|
||||
{
|
||||
private static readonly ProjectCollection ProjectCollection = new();
|
||||
private static readonly BuildManager UnrealSharpBuildManager = new("UnrealSharpBuildManager");
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
FUnrealSharpEditorModuleExporter.CallGetProjectPaths(out UnmanagedArray projectPaths);
|
||||
List<string> projectPathsList = projectPaths.ToListWithMarshaller(StringMarshaller.FromNative);
|
||||
|
||||
foreach (string projectPath in projectPathsList)
|
||||
{
|
||||
ProjectCollection.LoadProject(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe NativeBool Build(char* solutionPath,
|
||||
char* outputPath,
|
||||
char* buildConfiguration,
|
||||
LoggerVerbosity loggerVerbosity,
|
||||
IntPtr exceptionBuffer,
|
||||
NativeBool buildSolution)
|
||||
{
|
||||
try
|
||||
{
|
||||
string buildConfigurationString = new string(buildConfiguration);
|
||||
|
||||
if (buildSolution == NativeBool.True)
|
||||
{
|
||||
ErrorCollectingLogger logger = new ErrorCollectingLogger(loggerVerbosity);
|
||||
BuildParameters buildParameters = new(ProjectCollection)
|
||||
{
|
||||
Loggers = new List<ILogger> { logger }
|
||||
};
|
||||
|
||||
Dictionary<string, string?> globalProperties = new()
|
||||
{
|
||||
["Configuration"] = buildConfigurationString,
|
||||
};
|
||||
|
||||
BuildRequestData buildRequest = new BuildRequestData(
|
||||
new string(solutionPath),
|
||||
globalProperties,
|
||||
null,
|
||||
new[] { "Build" },
|
||||
null
|
||||
);
|
||||
|
||||
BuildResult result = UnrealSharpBuildManager.Build(buildParameters, buildRequest);
|
||||
if (result.OverallResult == BuildResultCode.Failure)
|
||||
{
|
||||
throw new Exception(logger.ErrorLog.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Weave(outputPath, buildConfigurationString);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
StringMarshaller.ToNative(exceptionBuffer, 0, exception.Message);
|
||||
return NativeBool.False;
|
||||
}
|
||||
|
||||
return NativeBool.True;
|
||||
}
|
||||
|
||||
static unsafe void Weave(char* outputPath, string buildConfiguration)
|
||||
{
|
||||
List<string> assemblyPaths = new();
|
||||
foreach (Project? projectFile in ProjectCollection.LoadedProjects
|
||||
.Where(p => p.GetPropertyValue("ExcludeFromWeaver") != "true"))
|
||||
{
|
||||
string projectName = Path.GetFileNameWithoutExtension(projectFile.FullPath);
|
||||
string assemblyPath = Path.Combine(projectFile.DirectoryPath, "bin",
|
||||
buildConfiguration, DotNetUtilities.DOTNET_MAJOR_VERSION_DISPLAY, projectName + ".dll");
|
||||
|
||||
assemblyPaths.Add(assemblyPath);
|
||||
}
|
||||
|
||||
WeaverOptions weaverOptions = new WeaverOptions
|
||||
{
|
||||
AssemblyPaths = assemblyPaths,
|
||||
OutputDirectory = new string(outputPath),
|
||||
};
|
||||
|
||||
Program.Weave(weaverOptions);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static void ForceManagedGC()
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe NativeBool OpenSolution(char* solutionPath, IntPtr exceptionBuffer)
|
||||
{
|
||||
try
|
||||
{
|
||||
string solutionFilePath = new (solutionPath);
|
||||
|
||||
if (!File.Exists(solutionFilePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Solution not found at path \"{solutionFilePath}\"");
|
||||
}
|
||||
|
||||
ProcessStartInfo? startInfo = null;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
startInfo = new ProcessStartInfo("cmd.exe", $"/c start \"\" \"{solutionFilePath}\"");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
startInfo = new ProcessStartInfo("open", solutionFilePath);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
startInfo = new ProcessStartInfo("xdg-open", solutionFilePath);
|
||||
}
|
||||
|
||||
if (startInfo == null)
|
||||
{
|
||||
throw new PlatformNotSupportedException("Unsupported platform.");
|
||||
}
|
||||
|
||||
startInfo.WorkingDirectory = Path.GetDirectoryName(solutionFilePath);
|
||||
startInfo.Environment["MsBuildExtensionPath"] = null;
|
||||
startInfo.Environment["MSBUILD_EXE_PATH"] = null;
|
||||
startInfo.Environment["MsBuildSDKsPath"] = null;
|
||||
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
StringMarshaller.ToNative(exceptionBuffer, 0, exception.Message);
|
||||
return NativeBool.False;
|
||||
}
|
||||
|
||||
return NativeBool.True;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
public static unsafe void AddProjectToCollection(char* projectPath)
|
||||
{
|
||||
string projectPathString = new string(projectPath);
|
||||
|
||||
if (ProjectCollection.LoadedProjects.All(p => p.FullPath != projectPathString))
|
||||
{
|
||||
ProjectCollection.LoadProject(projectPathString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FUnrealSharpEditor : IModuleInterface
|
||||
{
|
||||
public void StartupModule()
|
||||
{
|
||||
FManagedUnrealSharpEditorCallbacks callbacks = new FManagedUnrealSharpEditorCallbacks();
|
||||
FUnrealSharpEditorModuleExporter.CallInitializeUnrealSharpEditorCallbacks(callbacks);
|
||||
ManagedUnrealSharpEditorCallbacks.Initialize();
|
||||
}
|
||||
|
||||
public void ShutdownModule()
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.EditorSourceGenerators;
|
||||
|
||||
struct AssemblyClassInfo(string fullName, string filePath)
|
||||
{
|
||||
public string FullName = fullName;
|
||||
public string FilePath = filePath;
|
||||
}
|
||||
|
||||
[Generator]
|
||||
public class ClassFilePathGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static string GetRelativePath(string filePath)
|
||||
{
|
||||
filePath = filePath.Replace("\\", "/");
|
||||
|
||||
int index = filePath.IndexOf("/Script", StringComparison.OrdinalIgnoreCase);
|
||||
if (index >= 0)
|
||||
{
|
||||
return filePath.Substring(index);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax,
|
||||
static (context, _) =>
|
||||
{
|
||||
return context.SemanticModel.GetDeclaredSymbol(context.Node) is INamedTypeSymbol classSymbol
|
||||
? (new[] { new AssemblyClassInfo(classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), context.Node.SyntaxTree.FilePath) })
|
||||
: Array.Empty<AssemblyClassInfo>();
|
||||
})
|
||||
.SelectMany((results, _) => results)
|
||||
.Where(i => i.FilePath != null)
|
||||
.Collect()
|
||||
.Combine(context.CompilationProvider);
|
||||
|
||||
context.RegisterSourceOutput(syntaxProvider, (outputContext, info) =>
|
||||
{
|
||||
var (classes, compilation) = info;
|
||||
|
||||
Dictionary<string, string> classDictionary = new Dictionary<string, string>();
|
||||
|
||||
foreach (AssemblyClassInfo classInfo in classes)
|
||||
{
|
||||
string className = classInfo.FullName;
|
||||
string? relativeFilePath = GetRelativePath(classInfo.FilePath);
|
||||
|
||||
if (!string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(relativeFilePath))
|
||||
{
|
||||
classDictionary[className] = relativeFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder();
|
||||
|
||||
sourceBuilder.AppendLine("using System.Collections.Generic;");
|
||||
sourceBuilder.AppendLine("using System.Runtime.CompilerServices;");
|
||||
sourceBuilder.AppendLine($"namespace {compilation.AssemblyName};");
|
||||
|
||||
sourceBuilder.AppendLine("public static class ClassFileMap");
|
||||
sourceBuilder.AppendLine("{");
|
||||
|
||||
sourceBuilder.AppendLine(" [ModuleInitializer]");
|
||||
sourceBuilder.AppendLine(" public static void Initialize()");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
|
||||
foreach (KeyValuePair<string, string> kvp in classDictionary)
|
||||
{
|
||||
sourceBuilder.AppendLine($" AddClassFile(\"{kvp.Key}\", \"{kvp.Value}\");");
|
||||
}
|
||||
|
||||
sourceBuilder.AppendLine(" }");
|
||||
|
||||
sourceBuilder.AppendLine(" public unsafe static void AddClassFile(string className, string filePath)");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
sourceBuilder.AppendLine(" fixed (char* ptr1 = className)");
|
||||
sourceBuilder.AppendLine(" fixed (char* ptr2 = filePath)");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
sourceBuilder.AppendLine(" UnrealSharp.Interop.FCSTypeRegistryExporter.CallRegisterClassToFilePath(ptr1, ptr2);");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
|
||||
sourceBuilder.AppendLine("}");
|
||||
|
||||
outputContext.AddSource("ClassFileMap.generated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,76 @@
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace UnrealSharp.ExtensionSourceGenerators;
|
||||
|
||||
public class ActorComponentExtensionGenerator : ExtensionGenerator
|
||||
{
|
||||
public override void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
GenerateConstructMethod(ref builder, classSymbol);
|
||||
GenerateComponentGetter(ref builder, classSymbol);
|
||||
}
|
||||
|
||||
private void GenerateConstructMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
string fullTypeName = classSymbol.ToDisplayString();
|
||||
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Constructs a new component of the specified class, and attaches it to the specified actor.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"owner\">The actor to attach the component to.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"bManualAttachment\">If true, the component will not be attached to the actor's root component.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"relativeTransform\">The relative transform of the component to the actor.</param>");
|
||||
stringBuilder.AppendLine(" /// <returns>The constructed component.</returns>");
|
||||
stringBuilder.AppendLine($" public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner, bool bManualAttachment, FTransform relativeTransform)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" return owner.AddComponentByClass<{fullTypeName}>(bManualAttachment, relativeTransform);");
|
||||
stringBuilder.AppendLine(" }");
|
||||
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Constructs a new component of the specified class, and attaches it to the specified actor.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"owner\">The actor to attach the component to.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"componentClass\">The class of the component to construct.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"bManualAttachment\">If true, the component will not be attached to the actor's root component.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"relativeTransform\">The relative transform of the component to the actor.</param>");
|
||||
stringBuilder.AppendLine(" /// <returns>The constructed component.</returns>");
|
||||
stringBuilder.AppendLine($" public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner, TSubclassOf<UActorComponent> componentClass, bool bManualAttachment, FTransform relativeTransform)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" return ({fullTypeName}) owner.AddComponentByClass(componentClass, bManualAttachment, relativeTransform);");
|
||||
stringBuilder.AppendLine(" }");
|
||||
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Constructs a new component of the specified class, and attaches it to the specified actor.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"owner\">The actor to attach the component to.</param>");
|
||||
stringBuilder.AppendLine(" /// <returns>The constructed component.</returns>");
|
||||
stringBuilder.AppendLine($" public static {fullTypeName} Construct(UnrealSharp.Engine.AActor owner)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" return ({fullTypeName}) owner.AddComponentByClass(typeof({fullTypeName}), false, new FTransform());");
|
||||
stringBuilder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private void GenerateComponentGetter(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
string fullTypeName = classSymbol.ToDisplayString();
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Gets the component of the specified class attached to the specified actor.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"owner\">The actor to get the component from.</param>");
|
||||
stringBuilder.AppendLine(" /// <returns>The component if found, otherwise null.</returns>");
|
||||
stringBuilder.AppendLine($" public static new {fullTypeName}? Get(UnrealSharp.Engine.AActor owner)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" UActorComponent? foundComponent = owner.GetComponentByClass<{fullTypeName}>(typeof({fullTypeName}));");
|
||||
stringBuilder.AppendLine(" if (foundComponent != null)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" return ({fullTypeName}) foundComponent;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine(" return null;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace UnrealSharp.ExtensionSourceGenerators;
|
||||
|
||||
public class ActorExtensionGenerator : ExtensionGenerator
|
||||
{
|
||||
public override void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
GenerateSpawnMethod(ref builder, classSymbol);
|
||||
GenerateGetActorsOfClassMethod(ref builder, classSymbol);
|
||||
}
|
||||
|
||||
private void GenerateSpawnMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
string fullTypeName = classSymbol.ToDisplayString();
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Spawns an actor of the specified class.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"worldContextObject\">The object to spawn the actor in.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"actorClass\">The class of the actor to spawn.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"spawnTransform\">The transform to spawn the actor at.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"spawnMethod\">The method to handle collisions when spawning the actor.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"instigator\">The actor that caused the actor to be spawned.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"owner\">The actor that owns the spawned actor.</param>");
|
||||
stringBuilder.AppendLine(" /// <returns>The spawned actor.</returns>");
|
||||
stringBuilder.AppendLine($" public static {fullTypeName} Spawn(TSubclassOf<{fullTypeName}> actorClass = default, FTransform spawnTransform = default, UnrealSharp.Engine.ESpawnActorCollisionHandlingMethod spawnMethod = ESpawnActorCollisionHandlingMethod.Undefined, APawn? instigator = null, AActor? owner = null)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" return SpawnActor<{fullTypeName}>(actorClass, spawnTransform, spawnMethod, instigator, owner);");
|
||||
stringBuilder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private void GenerateGetActorsOfClassMethod(ref StringBuilder stringBuilder, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
string fullTypeName = classSymbol.ToDisplayString();
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine(" /// <summary>");
|
||||
stringBuilder.AppendLine(" /// Gets all actors of the specified class in the world.");
|
||||
stringBuilder.AppendLine(" /// </summary>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"worldContextObject\">The object to get the actors from.</param>");
|
||||
stringBuilder.AppendLine(" /// <param name=\"outActors\">The list to store the actors in.</param>");
|
||||
stringBuilder.AppendLine($" public static new void GetAllActorsOfClass(out IList<{fullTypeName}> outActors)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" UGameplayStatics.GetAllActorsOfClass(out outActors);");
|
||||
stringBuilder.AppendLine(" }");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.ExtensionSourceGenerators;
|
||||
|
||||
[Generator]
|
||||
public class ClassExtenderGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static bool IsA(ITypeSymbol classSymbol, ITypeSymbol otherSymbol)
|
||||
{
|
||||
var currentSymbol = classSymbol.BaseType;
|
||||
|
||||
while (currentSymbol != null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(currentSymbol, otherSymbol))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
currentSymbol = currentSymbol.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var actorExtensionGenerator = new ActorExtensionGenerator();
|
||||
var actorComponentExtensionGenerator = new ActorComponentExtensionGenerator();
|
||||
|
||||
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider<(INamedTypeSymbol Symbol, ExtensionGenerator Generator)>(
|
||||
static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null },
|
||||
(context, _) =>
|
||||
{
|
||||
if (context.SemanticModel.GetTypeInfo(context.Node).Type is not INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
return (null!, null!);
|
||||
}
|
||||
|
||||
if (IsA(typeSymbol, context.SemanticModel.Compilation.GetTypeByMetadataName("UnrealSharp.Engine.AActor")!))
|
||||
{
|
||||
return (typeSymbol, actorExtensionGenerator);
|
||||
}
|
||||
else if (IsA(typeSymbol, context.SemanticModel.Compilation.GetTypeByMetadataName("UnrealSharp.Engine.UActorComponent")!))
|
||||
{
|
||||
return (typeSymbol, actorComponentExtensionGenerator);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (null!, null!);
|
||||
}
|
||||
})
|
||||
.Where(classDecl => classDecl.Symbol != null);
|
||||
|
||||
context.RegisterSourceOutput(syntaxProvider, (outputContext, classDecl) =>
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("#nullable disable");
|
||||
stringBuilder.AppendLine();
|
||||
|
||||
stringBuilder.AppendLine("using UnrealSharp.Engine;");
|
||||
stringBuilder.AppendLine("using UnrealSharp.CoreUObject;");
|
||||
stringBuilder.AppendLine("using UnrealSharp;");
|
||||
stringBuilder.AppendLine();
|
||||
|
||||
stringBuilder.AppendLine($"namespace {classDecl.Symbol.ContainingNamespace};");
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine($"public partial class {classDecl.Symbol.Name}");
|
||||
stringBuilder.AppendLine("{");
|
||||
|
||||
classDecl.Generator.Generate(ref stringBuilder, classDecl.Symbol);
|
||||
|
||||
stringBuilder.AppendLine("}");
|
||||
outputContext.AddSource($"{classDecl.Symbol.Name}.generated.extension.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
|
||||
private class ClassSyntaxReceiver : ISyntaxReceiver
|
||||
{
|
||||
public List<ClassDeclarationSyntax> CandidateClasses { get; } = [];
|
||||
|
||||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||||
{
|
||||
if (syntaxNode is ClassDeclarationSyntax { BaseList: not null } classDeclaration)
|
||||
{
|
||||
CandidateClasses.Add(classDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace UnrealSharp.ExtensionSourceGenerators;
|
||||
|
||||
public abstract class ExtensionGenerator
|
||||
{
|
||||
public abstract void Generate(ref StringBuilder builder, INamedTypeSymbol classSymbol);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,7 @@
|
||||
namespace UnrealSharp.Log;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class CustomLog(ELogVerbosity verbosity = ELogVerbosity.Display) : Attribute
|
||||
{
|
||||
private ELogVerbosity _verbosity = verbosity;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using UnrealSharp.Binds;
|
||||
|
||||
namespace UnrealSharp.Log;
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FMsgExporter
|
||||
{
|
||||
public static delegate* unmanaged<char*, ELogVerbosity, char*, void> Log;
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
namespace UnrealSharp.Log;
|
||||
|
||||
public enum ELogVerbosity : byte
|
||||
{
|
||||
/** Not used */
|
||||
NoLogging = 0,
|
||||
|
||||
/** Always prints a fatal error to console (and log file) and crashes (even if logging is disabled) */
|
||||
Fatal,
|
||||
|
||||
/**
|
||||
* Prints an error to console (and log file).
|
||||
* Commandlets and the editor collect and report errors. Error messages result in commandlet failure.
|
||||
*/
|
||||
Error,
|
||||
|
||||
/**
|
||||
* Prints a warning to console (and log file).
|
||||
* Commandlets and the editor collect and report warnings. Warnings can be treated as an error.
|
||||
*/
|
||||
Warning,
|
||||
|
||||
/** Prints a message to console (and log file) */
|
||||
Display,
|
||||
|
||||
/** Prints a message to a log file (does not print to console) */
|
||||
Log,
|
||||
|
||||
/**
|
||||
* Prints a verbose message to a log file (if Verbose logging is enabled for the given category,
|
||||
* usually used for detailed logging)
|
||||
*/
|
||||
Verbose,
|
||||
|
||||
/**
|
||||
* Prints a verbose message to a log file (if VeryVerbose logging is enabled,
|
||||
* usually used for detailed logging that would otherwise spam output)
|
||||
*/
|
||||
VeryVerbose,
|
||||
|
||||
// Log masks and special Enum values
|
||||
|
||||
All = VeryVerbose,
|
||||
NumVerbosity,
|
||||
VerbosityMask = 0xf,
|
||||
SetColor = 0x40, // not actually a verbosity, used to set the color of an output device
|
||||
BreakOnLog = 0x80
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
namespace UnrealSharp.Log;
|
||||
|
||||
public static class UnrealLogger
|
||||
{
|
||||
public static void Log(string logName, string message, ELogVerbosity logVerbosity = ELogVerbosity.Display)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (char* logNamePtr = logName)
|
||||
fixed (char* stringPtr = message)
|
||||
{
|
||||
FMsgExporter.CallLog(logNamePtr, logVerbosity, stringPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogWarning(string logName, string message)
|
||||
{
|
||||
Log(logName, message, ELogVerbosity.Warning);
|
||||
}
|
||||
|
||||
public static void LogError(string logName, string message)
|
||||
{
|
||||
Log(logName, message, ELogVerbosity.Error);
|
||||
}
|
||||
|
||||
public static void LogFatal(string logName, string message)
|
||||
{
|
||||
Log(logName, message, ELogVerbosity.Fatal);
|
||||
}
|
||||
|
||||
public static void LogVerbose(string logName, string message)
|
||||
{
|
||||
Log(logName, message, ELogVerbosity.Verbose);
|
||||
}
|
||||
|
||||
public static void LogVeryVerbose(string logName, string message)
|
||||
{
|
||||
Log(logName, message, ELogVerbosity.VeryVerbose);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UnrealSharp.Binds\UnrealSharp.Binds.csproj" />
|
||||
<ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj">
|
||||
<OutputItemType>Analyzer</OutputItemType>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,6 @@
|
||||
using UnrealSharp.Log;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
[CustomLog]
|
||||
public static partial class LogUnrealSharpPlugins;
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Build.Locator;
|
||||
using UnrealSharp.Binds;
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.Shared;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
public static class Main
|
||||
{
|
||||
internal static DllImportResolver _dllImportResolver = null!;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static unsafe NativeBool InitializeUnrealSharp(char* workingDirectoryPath, nint assemblyPath, PluginsCallbacks* pluginCallbacks, IntPtr bindsCallbacks, IntPtr managedCallbacks)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
string dotnetSdk = DotNetUtilities.GetLatestDotNetSdkPath();
|
||||
MSBuildLocator.RegisterMSBuildPath(dotnetSdk);
|
||||
#endif
|
||||
|
||||
AppDomain.CurrentDomain.SetData("APP_CONTEXT_BASE_DIRECTORY", new string(workingDirectoryPath));
|
||||
|
||||
// Initialize plugin and managed callbacks
|
||||
*pluginCallbacks = PluginsCallbacks.Create();
|
||||
|
||||
NativeBinds.InitializeNativeBinds(bindsCallbacks);
|
||||
ManagedCallbacks.Initialize(managedCallbacks);
|
||||
|
||||
LogUnrealSharpPlugins.Log("UnrealSharp successfully setup!");
|
||||
return NativeBool.True;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogUnrealSharpPlugins.LogError($"Error initializing UnrealSharp: {ex.Message}");
|
||||
return NativeBool.False;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using UnrealSharp.Engine.Core.Modules;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
public class Plugin
|
||||
{
|
||||
public Plugin(AssemblyName assemblyName, bool isCollectible, string assemblyPath)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
AssemblyPath = assemblyPath;
|
||||
|
||||
string pluginLoadContextName = assemblyName.Name! + "_AssemblyLoadContext";
|
||||
LoadContext = new PluginLoadContext(pluginLoadContextName, new AssemblyDependencyResolver(assemblyPath), isCollectible);
|
||||
WeakRefLoadContext = new WeakReference(LoadContext);
|
||||
}
|
||||
|
||||
public AssemblyName AssemblyName { get; }
|
||||
public string AssemblyPath;
|
||||
|
||||
public PluginLoadContext? LoadContext { get; private set; }
|
||||
public WeakReference? WeakRefLoadContext { get ; }
|
||||
|
||||
public WeakReference? WeakRefAssembly { get; private set; }
|
||||
public List<IModuleInterface> ModuleInterfaces { get; } = [];
|
||||
|
||||
public bool IsLoadContextAlive
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
get => WeakRefLoadContext != null && WeakRefLoadContext.IsAlive;
|
||||
}
|
||||
|
||||
public bool Load()
|
||||
{
|
||||
if (LoadContext == null || (WeakRefAssembly != null && WeakRefAssembly.IsAlive))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Assembly assembly = LoadContext.LoadFromAssemblyName(AssemblyName);
|
||||
WeakRefAssembly = new WeakReference(assembly);
|
||||
|
||||
Type[] types = assembly.GetTypes();
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
if (type == typeof(IModuleInterface) || !typeof(IModuleInterface).IsAssignableFrom(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Activator.CreateInstance(type) is not IModuleInterface moduleInterface)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
moduleInterface.StartupModule();
|
||||
ModuleInterfaces.Add(moduleInterface);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Unload()
|
||||
{
|
||||
ShutdownModule();
|
||||
|
||||
if (LoadContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLoadContext.RemoveAssemblyFromCache(AssemblyName.Name);
|
||||
|
||||
LoadContext.Unload();
|
||||
LoadContext = null;
|
||||
WeakRefAssembly = null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void ShutdownModule()
|
||||
{
|
||||
foreach (IModuleInterface moduleInterface in ModuleInterfaces)
|
||||
{
|
||||
moduleInterface.ShutdownModule();
|
||||
}
|
||||
|
||||
ModuleInterfaces.Clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using UnrealSharp.Binds;
|
||||
using UnrealSharp.Core;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
public class PluginLoadContext : AssemblyLoadContext
|
||||
{
|
||||
public PluginLoadContext(string assemblyName, AssemblyDependencyResolver resolver, bool isCollectible) : base(assemblyName, isCollectible)
|
||||
{
|
||||
_resolver = resolver;
|
||||
}
|
||||
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
private static readonly Dictionary<string, WeakReference<Assembly>> LoadedAssemblies = new();
|
||||
|
||||
static PluginLoadContext()
|
||||
{
|
||||
AddAssembly(typeof(PluginLoader).Assembly);
|
||||
AddAssembly(typeof(NativeBinds).Assembly);
|
||||
AddAssembly(typeof(UnrealSharpObject).Assembly);
|
||||
AddAssembly(typeof(UnrealSharpModule).Assembly);
|
||||
}
|
||||
|
||||
private static void AddAssembly(Assembly assembly)
|
||||
{
|
||||
LoadedAssemblies[assembly.GetName().Name!] = new WeakReference<Assembly>(assembly);
|
||||
}
|
||||
|
||||
public static void RemoveAssemblyFromCache(string assemblyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoadedAssemblies.Remove(assemblyName);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyName.Name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (LoadedAssemblies.TryGetValue(assemblyName.Name, out WeakReference<Assembly>? weakRef) && weakRef.TryGetTarget(out Assembly? cachedAssembly))
|
||||
{
|
||||
return cachedAssembly;
|
||||
}
|
||||
|
||||
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
|
||||
if (string.IsNullOrEmpty(assemblyPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using FileStream assemblyFile = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
string pdbPath = Path.ChangeExtension(assemblyPath, ".pdb");
|
||||
|
||||
Assembly? loadedAssembly;
|
||||
if (!File.Exists(pdbPath))
|
||||
{
|
||||
loadedAssembly = LoadFromStream(assemblyFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var pdbFile = File.Open(pdbPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
loadedAssembly = LoadFromStream(assemblyFile, pdbFile);
|
||||
}
|
||||
|
||||
LoadedAssemblies[assemblyName.Name] = new WeakReference<Assembly>(loadedAssembly);
|
||||
return loadedAssembly;
|
||||
}
|
||||
|
||||
protected override nint LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
|
||||
return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : nint.Zero;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
public static class PluginLoader
|
||||
{
|
||||
public static readonly List<Plugin> LoadedPlugins = [];
|
||||
|
||||
public static Assembly? LoadPlugin(string assemblyPath, bool isCollectible)
|
||||
{
|
||||
try
|
||||
{
|
||||
AssemblyName assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath));
|
||||
|
||||
foreach (Plugin loadedPlugin in LoadedPlugins)
|
||||
{
|
||||
if (!loadedPlugin.IsLoadContextAlive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (loadedPlugin.WeakRefAssembly?.Target is not Assembly assembly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assembly.GetName() != assemblyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LogUnrealSharpPlugins.Log($"Plugin {assemblyName} is already loaded.");
|
||||
return assembly;
|
||||
}
|
||||
|
||||
Plugin plugin = new Plugin(assemblyName, isCollectible, assemblyPath);
|
||||
if (plugin.Load() && plugin.WeakRefAssembly != null && plugin.WeakRefAssembly.Target is Assembly loadedAssembly)
|
||||
{
|
||||
LoadedPlugins.Add(plugin);
|
||||
LogUnrealSharpPlugins.Log($"Successfully loaded plugin: {assemblyName}");
|
||||
return loadedAssembly;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to load plugin: {assemblyName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogUnrealSharpPlugins.LogError($"An error occurred while loading the plugin: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static WeakReference? RemovePlugin(string assemblyName)
|
||||
{
|
||||
foreach (Plugin loadedPlugin in LoadedPlugins)
|
||||
{
|
||||
// Trying to resolve the weakptr to the assembly here will cause unload issues, so we compare names instead
|
||||
if (!loadedPlugin.IsLoadContextAlive || loadedPlugin.AssemblyName.Name != assemblyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
loadedPlugin.Unload();
|
||||
LoadedPlugins.Remove(loadedPlugin);
|
||||
return loadedPlugin.WeakRefLoadContext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool UnloadPlugin(string assemblyPath)
|
||||
{
|
||||
string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
|
||||
WeakReference? assemblyLoadContext = RemovePlugin(assemblyName);
|
||||
|
||||
if (assemblyLoadContext == null)
|
||||
{
|
||||
LogUnrealSharpPlugins.Log($"Plugin {assemblyName} is not loaded or already unloaded.");
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogUnrealSharpPlugins.Log($"Unloading plugin {assemblyName}...");
|
||||
|
||||
int startTimeMs = Environment.TickCount;
|
||||
bool takingTooLong = false;
|
||||
|
||||
while (assemblyLoadContext.IsAlive)
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
if (!assemblyLoadContext.IsAlive)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int elapsedTimeMs = Environment.TickCount - startTimeMs;
|
||||
|
||||
if (!takingTooLong && elapsedTimeMs >= 200)
|
||||
{
|
||||
takingTooLong = true;
|
||||
LogUnrealSharpPlugins.LogError($"Unloading {assemblyName} is taking longer than expected...");
|
||||
}
|
||||
else if (elapsedTimeMs >= 1000)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to unload {assemblyName}. Possible causes: Strong GC handles, running threads, etc.");
|
||||
}
|
||||
}
|
||||
|
||||
LogUnrealSharpPlugins.Log($"{assemblyName} unloaded successfully!");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogUnrealSharpPlugins.LogError($"An error occurred while unloading the plugin: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Plugin? FindPluginByName(string assemblyName)
|
||||
{
|
||||
foreach (Plugin loadedPlugin in LoadedPlugins)
|
||||
{
|
||||
if (loadedPlugin.AssemblyName.Name == assemblyName)
|
||||
{
|
||||
return loadedPlugin;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnrealSharp.Core;
|
||||
|
||||
namespace UnrealSharp.Plugins;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct PluginsCallbacks
|
||||
{
|
||||
public delegate* unmanaged<char*, NativeBool, nint> LoadPlugin;
|
||||
public delegate* unmanaged<char*, NativeBool> UnloadPlugin;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static nint ManagedLoadPlugin(char* assemblyPath, NativeBool isCollectible)
|
||||
{
|
||||
Assembly? newPlugin = PluginLoader.LoadPlugin(new string(assemblyPath), isCollectible.ToManagedBool());
|
||||
|
||||
if (newPlugin == null)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
};
|
||||
|
||||
return GCHandle.ToIntPtr(GCHandleUtilities.AllocateStrongPointer(newPlugin, newPlugin));
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static NativeBool ManagedUnloadPlugin(char* assemblyPath)
|
||||
{
|
||||
string assemblyPathStr = new(assemblyPath);
|
||||
return PluginLoader.UnloadPlugin(assemblyPathStr).ToNativeBool();
|
||||
}
|
||||
|
||||
public static PluginsCallbacks Create()
|
||||
{
|
||||
return new PluginsCallbacks
|
||||
{
|
||||
LoadPlugin = &ManagedLoadPlugin,
|
||||
UnloadPlugin = &ManagedUnloadPlugin,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<OutputPath>../../../Binaries/Managed</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<NoWarn>$(NoWarn);1570;0649;0169;0108;0109</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DefineConstants>WITH_EDITOR</DefineConstants>
|
||||
<DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants>
|
||||
<DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UnrealSharp.Core\UnrealSharp.Core.csproj" />
|
||||
<ProjectReference Include="..\UnrealSharp.Log\UnrealSharp.Log.csproj" />
|
||||
|
||||
<ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj">
|
||||
<OutputItemType>Analyzer</OutputItemType>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
|
||||
<ProjectReference Include="..\UnrealSharp\UnrealSharp.csproj" />
|
||||
|
||||
<PackageReference Include="Microsoft.Build.Locator" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Shared\DotNetUtilities.cs" Link="..\..\Shared\DotNetUtilities.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,50 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace UnrealSharp;
|
||||
|
||||
public class UnrealSharpDllImportResolver(IntPtr internalHandle)
|
||||
{
|
||||
public IntPtr OnResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (libraryName != "__Internal")
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return Win32.GetModuleHandle(IntPtr.Zero);
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return internalHandle;
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return MacOS.dlopen(IntPtr.Zero, MacOS.RTLD_LAZY);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static class MacOS
|
||||
{
|
||||
private const string SystemLibrary = "/usr/lib/libSystem.dylib";
|
||||
|
||||
public const int RTLD_LAZY = 1;
|
||||
|
||||
[DllImport(SystemLibrary)]
|
||||
public static extern IntPtr dlopen(IntPtr path, int mode);
|
||||
}
|
||||
|
||||
private static class Win32
|
||||
{
|
||||
private const string SystemLibrary = "Kernel32.dll";
|
||||
|
||||
[DllImport(SystemLibrary)]
|
||||
public static extern IntPtr GetModuleHandle(IntPtr lpModuleName);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
; Shipped analyzer releases
|
||||
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
; Unshipped analyzer release
|
||||
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
--------|----------|----------|-------
|
||||
PrefixAnalyzer | Naming | Error | UnrealTypeAnalyzer
|
||||
US0001 | UnrealSharp | Error | UStaticLambdaAnalyzer
|
||||
US0002 | Category | Error | UEnumAnalyzer
|
||||
US0003 | Category | Error | UInterfaceAnalyzer
|
||||
US0004 | Category | Error | UInterfaceAnalyzer
|
||||
US0006 | Category | Error | UnrealTypeAnalyzer
|
||||
US0007 | Category | Error | UObjectCreationAnalyzer
|
||||
US0008 | Category | Error | UObjectCreationAnalyzer
|
||||
US0009 | Category | Error | UObjectCreationAnalyzer
|
||||
US0010 | Category | Error | UObjectCreationAnalyzer
|
||||
US0011 | Category | Error | UObjectCreationAnalyzer
|
||||
US0012 | Usage | Error | UFunctionConflictAnalyzer
|
||||
US0013 | Category | Error | DefaultComponentAnalyzer
|
||||
US0014 | Category | Error | DefaultComponentAnalyzer
|
||||
@ -0,0 +1,182 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public static class AnalyzerStatics
|
||||
{
|
||||
public const string UStructAttribute = "UStructAttribute";
|
||||
public const string UEnumAttribute = "UEnumAttribute";
|
||||
public const string UClassAttribute = "UClassAttribute";
|
||||
public const string UInterfaceAttribute = "UInterfaceAttribute";
|
||||
public const string UMultiDelegateAttribute = "UMultiDelegateAttribute";
|
||||
public const string USingleDelegateAttribute = "USingleDelegateAttribute";
|
||||
|
||||
public const string GeneratedTypeAttribute = "GeneratedTypeAttribute";
|
||||
|
||||
public const string UPropertyAttribute = "UPropertyAttribute";
|
||||
public const string UFunctionAttribute = "UFunctionAttribute";
|
||||
|
||||
public const string BindingAttribute = "BindingAttribute";
|
||||
|
||||
public const string UObject = "UObject";
|
||||
public const string AActor = "AActor";
|
||||
|
||||
public const string DefaultComponent = "DefaultComponent";
|
||||
public const string New = "new";
|
||||
public const string UActorComponent = "UActorComponent";
|
||||
public const string USceneComponent = "USceneComponent";
|
||||
public const string UUserWidget = "UUserWidget";
|
||||
|
||||
private const string ContainerNamespace = "System.Collections.Generic";
|
||||
private static readonly string[] ContainerInterfaces =
|
||||
{
|
||||
"IList",
|
||||
"IReadOnlyList",
|
||||
"IDictionary",
|
||||
"IReadOnlyDictionary",
|
||||
"ISet",
|
||||
"IReadOnlySet",
|
||||
};
|
||||
|
||||
public static bool HasAttribute(ISymbol symbol, string attributeName)
|
||||
{
|
||||
foreach (var attribute in symbol.GetAttributes())
|
||||
{
|
||||
if (attribute.AttributeClass?.Name == attributeName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetAttribute(ISymbol symbol, string attributeName, out AttributeData? attribute)
|
||||
{
|
||||
attribute = symbol.GetAttributes()
|
||||
.FirstOrDefault(x => x.AttributeClass is not null && x.AttributeClass.Name == attributeName);
|
||||
|
||||
return attribute is not null;
|
||||
}
|
||||
|
||||
public static bool HasAttribute(MemberDeclarationSyntax memberDecl, string attributeName)
|
||||
{
|
||||
foreach (var attrList in memberDecl.AttributeLists)
|
||||
{
|
||||
foreach (var attr in attrList.Attributes)
|
||||
{
|
||||
if (attr.Name.ToString().Contains(attributeName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool InheritsFrom(IPropertySymbol propertySymbol, string baseTypeName)
|
||||
{
|
||||
return propertySymbol.Type is INamedTypeSymbol namedTypeSymbol && InheritsFrom(namedTypeSymbol, baseTypeName);
|
||||
}
|
||||
|
||||
public static bool InheritsFrom(INamedTypeSymbol symbol, string baseTypeName)
|
||||
{
|
||||
INamedTypeSymbol? currentSymbol = symbol;
|
||||
|
||||
while (currentSymbol != null)
|
||||
{
|
||||
if (currentSymbol.Name == baseTypeName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
currentSymbol = currentSymbol.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsDefaultComponent(AttributeData? attributeData)
|
||||
{
|
||||
if (attributeData?.AttributeClass?.Name != UPropertyAttribute) return false;
|
||||
|
||||
var argument = attributeData.NamedArguments.FirstOrDefault(x => x.Key == DefaultComponent);
|
||||
if (string.IsNullOrWhiteSpace(argument.Key)) return false;
|
||||
|
||||
return argument.Value.Value is true;
|
||||
}
|
||||
|
||||
public static bool IsNewKeywordInstancingOperation(IObjectCreationOperation operation, out Location? location)
|
||||
{
|
||||
location = null;
|
||||
if (operation.Syntax is not ObjectCreationExpressionSyntax objectCreationExpression)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
location = objectCreationExpression.NewKeyword.GetLocation();
|
||||
return objectCreationExpression.NewKeyword.ValueText == New;
|
||||
}
|
||||
|
||||
public static bool IsContainerInterface(ITypeSymbol symbol)
|
||||
{
|
||||
var namespaceName = symbol.ContainingNamespace.ToString();
|
||||
return namespaceName.Equals(ContainerNamespace, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
ContainerInterfaces.Contains(symbol.Name);
|
||||
}
|
||||
|
||||
public static string GenerateUniqueMethodName(ClassDeclarationSyntax containingClass, string suffix)
|
||||
{
|
||||
int counter = 1;
|
||||
ImmutableHashSet<string> existingNames = containingClass.Members
|
||||
.OfType<MethodDeclarationSyntax>()
|
||||
.Select(m => m.Identifier.ValueText)
|
||||
.ToImmutableHashSet();
|
||||
|
||||
string methodName;
|
||||
do
|
||||
{
|
||||
methodName = $"Generated_{suffix}_{counter++}";
|
||||
}
|
||||
while (existingNames.Contains(methodName));
|
||||
|
||||
return methodName;
|
||||
}
|
||||
|
||||
public static string GetFullNamespace(this CSharpSyntaxNode declaration)
|
||||
{
|
||||
var namespaceNode = declaration.FirstAncestorOrSelf<BaseNamespaceDeclarationSyntax>();
|
||||
var namespaceBuilder = new StringBuilder();
|
||||
if (namespaceNode != null)
|
||||
{
|
||||
namespaceBuilder.Append(namespaceNode.Name.ToString());
|
||||
var currentNamespace = namespaceNode.Parent as BaseNamespaceDeclarationSyntax;
|
||||
while (currentNamespace != null)
|
||||
{
|
||||
namespaceBuilder.Insert(0, $"{currentNamespace.Name}.");
|
||||
currentNamespace = currentNamespace.Parent as BaseNamespaceDeclarationSyntax;
|
||||
}
|
||||
}
|
||||
|
||||
return namespaceBuilder.ToString();
|
||||
}
|
||||
|
||||
public static string? GetAnnotatedTypeName(this TypeSyntax? type, SemanticModel model)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var typeInfo = model.GetTypeInfo(type).Type;
|
||||
return type is NullableTypeSyntax ?
|
||||
typeInfo?.WithNullableAnnotation(NullableAnnotation.Annotated).ToString() : typeInfo?.ToString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,379 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
internal readonly struct AsyncMethodInfo(
|
||||
ClassDeclarationSyntax parentClass,
|
||||
MethodDeclarationSyntax method,
|
||||
string ns,
|
||||
TypeSyntax? returnType,
|
||||
IReadOnlyDictionary<string, string> metadata,
|
||||
bool nullableAwareable,
|
||||
bool returnsValueTask = false)
|
||||
{
|
||||
public ClassDeclarationSyntax ParentClass { get; } = parentClass;
|
||||
public MethodDeclarationSyntax Method { get; } = method;
|
||||
public string Namespace { get; } = ns;
|
||||
public TypeSyntax? ReturnType { get; } = returnType;
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; } = metadata;
|
||||
public bool NullableAwareable { get; } = nullableAwareable;
|
||||
public bool ReturnsValueTask { get; } = returnsValueTask;
|
||||
}
|
||||
|
||||
[Generator]
|
||||
public class AsyncWrapperGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var asyncMethods = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax } m && m.AttributeLists.Count > 0,
|
||||
static (syntaxContext, _) => GetAsyncMethodInfo(syntaxContext))
|
||||
.Where(static m => m.HasValue)
|
||||
.Select(static (m, _) => m!.Value);
|
||||
|
||||
var asyncMethodsWithCompilation = asyncMethods.Combine(context.CompilationProvider);
|
||||
|
||||
context.RegisterSourceOutput(asyncMethodsWithCompilation, static (spc, pair) =>
|
||||
{
|
||||
var methodInfo = pair.Left;
|
||||
var compilation = pair.Right;
|
||||
var source = Generate(methodInfo, compilation);
|
||||
if (!string.IsNullOrEmpty(source))
|
||||
{
|
||||
spc.AddSource($"{methodInfo.ParentClass.Identifier.Text}.{methodInfo.Method.Identifier.Text}.generated.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string Generate(AsyncMethodInfo asyncMethodInfo, Compilation compilation)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(asyncMethodInfo.Method.SyntaxTree);
|
||||
var method = asyncMethodInfo.Method;
|
||||
|
||||
var cancellationTokenType = compilation.GetTypeByMetadataName("System.Threading.CancellationToken");
|
||||
ParameterSyntax? cancellationTokenParameter = null;
|
||||
|
||||
HashSet<string> namespaces = new() { "UnrealSharp", "UnrealSharp.Attributes", "UnrealSharp.UnrealSharpCore" };
|
||||
foreach (var parameter in method.ParameterList.Parameters)
|
||||
{
|
||||
if (parameter.Type == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var typeInfo = model.GetTypeInfo(parameter.Type);
|
||||
var typeSymbol = typeInfo.Type;
|
||||
if (SymbolEqualityComparer.Default.Equals(typeSymbol, cancellationTokenType))
|
||||
{
|
||||
cancellationTokenParameter = parameter;
|
||||
}
|
||||
|
||||
if (typeSymbol == null || typeSymbol.ContainingNamespace == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeSymbol is INamedTypeSymbol nts && nts.IsGenericType)
|
||||
{
|
||||
namespaces.UnionWith(nts.TypeArguments.Select(t => t.ContainingNamespace.ToDisplayString()));
|
||||
}
|
||||
|
||||
namespaces.Add(typeSymbol.ContainingNamespace.ToDisplayString());
|
||||
|
||||
namespaces.UnionWith(parameter.AttributeLists.SelectMany(a => a.Attributes)
|
||||
.Select(a => model.GetTypeInfo(a).Type)
|
||||
.Where(type => type is not null)
|
||||
.Where(type => type!.ContainingNamespace is not null)
|
||||
.Select(type => type!.ContainingNamespace.ToDisplayString()));
|
||||
}
|
||||
|
||||
var returnTypeName = asyncMethodInfo.ReturnType.GetAnnotatedTypeName(model);
|
||||
var actionClassName = $"{asyncMethodInfo.ParentClass.Identifier.Text}{method.Identifier.Text}Action";
|
||||
var actionBaseClassName = cancellationTokenParameter != null ? "UCSCancellableAsyncAction" : "UCSBlueprintAsyncActionBase";
|
||||
var delegateName = $"{actionClassName}Delegate";
|
||||
var taskTypeName = asyncMethodInfo.ReturnType != null ? $"Task<{returnTypeName}>" : "Task";
|
||||
var paramNameList = string.Join(", ", method.ParameterList.Parameters.Select(p => p == cancellationTokenParameter ? "cancellationToken" : p.Identifier.Text));
|
||||
var paramDeclListNoCancellationToken = string.Join(", ", method.ParameterList.Parameters.Where(p => p != cancellationTokenParameter));
|
||||
|
||||
var metadataAttributeList = string.Join(", ", asyncMethodInfo.Metadata.Select(static pair => $"UMetaData({pair.Key}, {pair.Value})"));
|
||||
if (string.IsNullOrEmpty(metadataAttributeList))
|
||||
{
|
||||
metadataAttributeList = "UMetaData(\"BlueprintInternalUseOnly\", \"true\")";
|
||||
}
|
||||
else
|
||||
{
|
||||
metadataAttributeList = $"UMetaData(\"BlueprintInternalUseOnly\", \"true\"), {metadataAttributeList}";
|
||||
}
|
||||
|
||||
var isStatic = method.Modifiers.Any(static x => x.IsKind(SyntaxKind.StaticKeyword));
|
||||
if (!isStatic)
|
||||
{
|
||||
metadataAttributeList = $"UMetaData(\"DefaultToSelf\", \"Target\"), {metadataAttributeList}";
|
||||
}
|
||||
|
||||
var sourceBuilder = new StringBuilder();
|
||||
var nullableAnnotation = "?";
|
||||
var nullableSuppression = "!";
|
||||
if (asyncMethodInfo.NullableAwareable)
|
||||
{
|
||||
sourceBuilder.AppendLine("#nullable enable");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine("#nullable disable");
|
||||
nullableAnnotation = "";
|
||||
nullableSuppression = "";
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
foreach (var ns in namespaces)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ns))
|
||||
{
|
||||
sourceBuilder.AppendLine($"using {ns};");
|
||||
}
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($"namespace {asyncMethodInfo.Namespace};");
|
||||
sourceBuilder.AppendLine();
|
||||
if (asyncMethodInfo.ReturnType != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($"public delegate void {delegateName}({returnTypeName} Result, string{nullableAnnotation} Exception);");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($"public delegate void {delegateName}(string{nullableAnnotation} Exception);");
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($"public class U{delegateName} : MulticastDelegate<{delegateName}>");
|
||||
sourceBuilder.AppendLine("{");
|
||||
if (asyncMethodInfo.ReturnType != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" protected void Invoker({returnTypeName} Result, string{nullableAnnotation} Exception)");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
sourceBuilder.AppendLine(" ProcessDelegate(IntPtr.Zero);");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" protected void Invoker(string{nullableAnnotation} Exception)");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
sourceBuilder.AppendLine(" ProcessDelegate(IntPtr.Zero);");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" protected override {delegateName} GetInvoker()");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
sourceBuilder.AppendLine(" return Invoker;");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
sourceBuilder.AppendLine("}");
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine("[UClass]");
|
||||
sourceBuilder.AppendLine($"public class {actionClassName} : {actionBaseClassName}");
|
||||
sourceBuilder.AppendLine("{");
|
||||
sourceBuilder.AppendLine($" private {taskTypeName}{nullableAnnotation} task;");
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine(" private readonly CancellationTokenSource cancellationTokenSource = new();");
|
||||
sourceBuilder.AppendLine($" private Func<CancellationToken, {taskTypeName}>{nullableAnnotation} asyncDelegate;");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" private Func<{taskTypeName}>{nullableAnnotation} asyncDelegate;");
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" [UProperty(PropertyFlags.BlueprintAssignable)]");
|
||||
sourceBuilder.AppendLine($" public TMulticastDelegate<{delegateName}>{nullableAnnotation} Completed {{ get; set; }}");
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" [UProperty(PropertyFlags.BlueprintAssignable)]");
|
||||
sourceBuilder.AppendLine($" public TMulticastDelegate<{delegateName}>{nullableAnnotation} Failed {{ get; set; }}");
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" [UFunction(FunctionFlags.BlueprintCallable), {metadataAttributeList}]");
|
||||
string conversion = asyncMethodInfo.ReturnsValueTask ? ".AsTask()" : "";
|
||||
if (isStatic)
|
||||
{
|
||||
sourceBuilder.AppendLine($" public static {actionClassName} {method.Identifier.Text}({paramDeclListNoCancellationToken})");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
sourceBuilder.AppendLine($" var action = NewObject<{actionClassName}>(GetTransientPackage());");
|
||||
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" action.asyncDelegate = (cancellationToken) => {asyncMethodInfo.ParentClass.Identifier.Text}.{method.Identifier.Text}({paramNameList}){conversion};");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" action.asyncDelegate = () => {asyncMethodInfo.ParentClass.Identifier.Text}.{method.Identifier.Text}({paramNameList}){conversion};");
|
||||
}
|
||||
sourceBuilder.AppendLine($" return action;");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(paramDeclListNoCancellationToken))
|
||||
{
|
||||
sourceBuilder.AppendLine($" public static {actionClassName} {method.Identifier.Text}({asyncMethodInfo.ParentClass.Identifier.Text} Target)");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" public static {actionClassName} {method.Identifier.Text}({asyncMethodInfo.ParentClass.Identifier.Text} Target, {paramDeclListNoCancellationToken})");
|
||||
}
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
sourceBuilder.AppendLine($" var action = NewObject<{actionClassName}>(Target);");
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" action.asyncDelegate = (cancellationToken) => Target.{method.Identifier.Text}({paramNameList}){conversion};");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" action.asyncDelegate = () => Target.{method.Identifier.Text}({paramNameList}){conversion};");
|
||||
}
|
||||
sourceBuilder.AppendLine($" return action;");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" protected override void Activate()");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
sourceBuilder.AppendLine($" if (asyncDelegate == null) {{ throw new InvalidOperationException(\"AsyncDelegate was null\"); }}");
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" task = asyncDelegate(cancellationTokenSource.Token);");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" task = asyncDelegate();");
|
||||
}
|
||||
sourceBuilder.AppendLine($" task.ContinueWith(OnTaskCompleted);");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" protected override void Cancel()");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
sourceBuilder.AppendLine($" cancellationTokenSource.Cancel();");
|
||||
sourceBuilder.AppendLine($" base.Cancel();");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
}
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($" private void OnTaskCompleted({taskTypeName} t)");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
// sourceBuilder.AppendLine($" if (!IsDestroyed) {{ PrintString($\"OnTaskCompleted for {{this}} on {{UnrealSynchronizationContext.CurrentThread}}\"); }}");
|
||||
sourceBuilder.AppendLine($" if (UnrealSynchronizationContext.CurrentThread != NamedThread.GameThread)");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
sourceBuilder.AppendLine($" new UnrealSynchronizationContext(NamedThread.GameThread, t).Post(_ => OnTaskCompleted(t), null);");
|
||||
sourceBuilder.AppendLine($" return;");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
if (cancellationTokenParameter != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" if (cancellationTokenSource.IsCancellationRequested || IsDestroyed) {{ return; }}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" if (IsDestroyed) {{ return; }}");
|
||||
}
|
||||
sourceBuilder.AppendLine($" if (t.IsFaulted)");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
if (asyncMethodInfo.ReturnType != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" Failed?.InnerDelegate.Invoke(default{nullableSuppression}, t.Exception?.ToString() ?? \"Faulted without exception\");");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" Failed?.InnerDelegate.Invoke(t.Exception?.ToString() ?? \"Faulted without exception\");");
|
||||
}
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
sourceBuilder.AppendLine($" else");
|
||||
sourceBuilder.AppendLine($" {{");
|
||||
if (asyncMethodInfo.ReturnType != null)
|
||||
{
|
||||
sourceBuilder.AppendLine($" Completed?.InnerDelegate.Invoke(t.Result, null);");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine($" Completed?.InnerDelegate.Invoke(null);");
|
||||
}
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
sourceBuilder.AppendLine($" }}");
|
||||
sourceBuilder.AppendLine($"}}");
|
||||
|
||||
return sourceBuilder.ToString();
|
||||
}
|
||||
|
||||
private static AsyncMethodInfo? GetAsyncMethodInfo(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is not MethodDeclarationSyntax methodDeclaration)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var hasUFunctionAttribute = methodDeclaration.AttributeLists
|
||||
.SelectMany(a => a.Attributes)
|
||||
.Any(a => a.Name.ToString() == "UFunction");
|
||||
if (!hasUFunctionAttribute)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TypeSyntax? returnType;
|
||||
bool returnsValueTask;
|
||||
switch (methodDeclaration.ReturnType)
|
||||
{
|
||||
case IdentifierNameSyntax { Identifier.ValueText: "Task" }:
|
||||
// Method returns non-generic task, e.g. without return value
|
||||
returnType = null;
|
||||
returnsValueTask = false;
|
||||
break;
|
||||
case GenericNameSyntax { Identifier.ValueText: "Task" } genericTask:
|
||||
// Method returns generic task, e.g. with return value
|
||||
returnType = genericTask.TypeArgumentList.Arguments.Single();
|
||||
returnsValueTask = false;
|
||||
break;
|
||||
case IdentifierNameSyntax { Identifier.ValueText: "ValueTask" }:
|
||||
// Method returns non-generic task, e.g. without return value
|
||||
returnType = null;
|
||||
returnsValueTask = true;
|
||||
break;
|
||||
case GenericNameSyntax { Identifier.ValueText: "ValueTask" } genericValueTask:
|
||||
// Method returns generic task, e.g. with return value
|
||||
returnType = genericValueTask.TypeArgumentList.Arguments.Single();
|
||||
returnsValueTask = true;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
string namespaceName = methodDeclaration.GetFullNamespace();
|
||||
if (string.IsNullOrEmpty(namespaceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadataAttributes = methodDeclaration.AttributeLists
|
||||
.SelectMany(a => a.Attributes)
|
||||
.Where(a => a.Name.ToString() == "UMetaData" || a.GetFullNamespace() == "UnrealSharp.Attributes.MetaData");
|
||||
|
||||
Dictionary<string, string> metadata = new();
|
||||
foreach (var metadataAttribute in metadataAttributes)
|
||||
{
|
||||
if (metadataAttribute.ArgumentList == null || metadataAttribute.ArgumentList.Arguments.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var key = metadataAttribute.ArgumentList.Arguments[0].Expression.ToString();
|
||||
var value = metadataAttribute.ArgumentList.Arguments.Count > 1 ? metadataAttribute.ArgumentList.Arguments[1].Expression.ToString() : "";
|
||||
metadata[key] = value;
|
||||
}
|
||||
|
||||
return new AsyncMethodInfo(classDeclaration, methodDeclaration, namespaceName, returnType, metadata,
|
||||
context.SemanticModel
|
||||
.GetNullableContext(context.Node.Span.Start)
|
||||
.HasFlag(NullableContext.AnnotationsEnabled), returnsValueTask);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class DefaultComponentAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
DefaultComponentRule,
|
||||
DefaultComponentSetterRule
|
||||
);
|
||||
|
||||
public const string DefaultComponentAnalyzerId = "US0013";
|
||||
private static readonly LocalizableString DefaultComponentAnalyzerTitle = "UnrealSharp DefaultComponent Analyzer";
|
||||
private static readonly LocalizableString DefaultComponentAnalyzerMessageFormat = "{0} is a DefaultComponent, which is not inherit from UActorComponent";
|
||||
private static readonly LocalizableString DefaultComponentAnalyzerDescription = "Ensures property type marked as DefaultComponent inherits from UActorComponent.";
|
||||
private static readonly DiagnosticDescriptor DefaultComponentRule = new(DefaultComponentAnalyzerId, DefaultComponentAnalyzerTitle, DefaultComponentAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DefaultComponentAnalyzerDescription);
|
||||
|
||||
public const string DefaultComponentSetterAnalyzerId = "US0014";
|
||||
private static readonly LocalizableString DefaultComponentSetterAnalyzerTitle = "UnrealSharp DefaultComponent Setter Analyzer";
|
||||
private static readonly LocalizableString DefaultComponentSetterAnalyzerMessageFormat = "{0} is a DefaultComponent without setter";
|
||||
private static readonly LocalizableString DefaultComponentSetterAnalyzerDescription = "Ensures property marked as DefaultComponent has setter.";
|
||||
private static readonly DiagnosticDescriptor DefaultComponentSetterRule = new(DefaultComponentSetterAnalyzerId, DefaultComponentSetterAnalyzerTitle, DefaultComponentSetterAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DefaultComponentSetterAnalyzerDescription);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.RegisterSymbolAction(AnalyzeClassProperties, SymbolKind.Property);
|
||||
}
|
||||
|
||||
private static void AnalyzeClassProperties(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol.ContainingType.TypeKind != TypeKind.Class)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Symbol is not IPropertySymbol propertySymbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AnalyzerStatics.TryGetAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute, out var propertyAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDefaultComponent = AnalyzerStatics.IsDefaultComponent(propertyAttribute);
|
||||
bool inheritFromActorComponent = AnalyzerStatics.InheritsFrom(propertySymbol, AnalyzerStatics.UActorComponent);
|
||||
|
||||
if (isDefaultComponent && !inheritFromActorComponent)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DefaultComponentRule, propertySymbol.Locations[0], propertySymbol.Name));
|
||||
}
|
||||
|
||||
if (isDefaultComponent && propertySymbol.SetMethod is null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(DefaultComponentSetterRule, propertySymbol.Locations[0], propertySymbol.Name));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UFunctionConflictAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
"US0012",
|
||||
"Conflicting UFunction Attribute",
|
||||
"Method '{0}' in class '{1}' should not have a UFunction attribute because it is already defined in the interface '{2}'",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
|
||||
}
|
||||
|
||||
private static void AnalyzeMethod(SymbolAnalysisContext context)
|
||||
{
|
||||
IMethodSymbol methodSymbol = (IMethodSymbol) context.Symbol;
|
||||
|
||||
foreach (var implementedMethod in methodSymbol.ExplicitInterfaceImplementations)
|
||||
{
|
||||
CheckForUFunctionConflict(context, methodSymbol, implementedMethod);
|
||||
}
|
||||
|
||||
foreach (INamedTypeSymbol? typeSymbol in methodSymbol.ContainingType.AllInterfaces)
|
||||
{
|
||||
foreach (IMethodSymbol? interfaceMethod in typeSymbol.GetMembers().OfType<IMethodSymbol>())
|
||||
{
|
||||
ISymbol? implementation = methodSymbol.ContainingType.FindImplementationForInterfaceMember(interfaceMethod);
|
||||
if (SymbolEqualityComparer.Default.Equals(implementation, methodSymbol))
|
||||
{
|
||||
CheckForUFunctionConflict(context, methodSymbol, interfaceMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckForUFunctionConflict(
|
||||
SymbolAnalysisContext context,
|
||||
IMethodSymbol implementationMethod,
|
||||
IMethodSymbol interfaceMethod)
|
||||
{
|
||||
bool HasUFunction(IMethodSymbol method)
|
||||
{
|
||||
return method.GetAttributes().Any(attr =>
|
||||
attr.AttributeClass?.Name == AnalyzerStatics.UFunctionAttribute);
|
||||
}
|
||||
|
||||
if (!HasUFunction(interfaceMethod) || !HasUFunction(implementationMethod))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Diagnostic diagnostic = Diagnostic.Create(
|
||||
Rule,
|
||||
implementationMethod.Locations[0],
|
||||
implementationMethod.Name,
|
||||
implementationMethod.ContainingType.Name,
|
||||
interfaceMethod.ContainingType.Name);
|
||||
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Rename;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnrealTypeCodeFixProvider)), Shared]
|
||||
public class UnrealTypeCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
|
||||
UnrealTypeAnalyzer.StructAnalyzerId,
|
||||
UnrealTypeAnalyzer.ClassAnalyzerId,
|
||||
UnrealTypeAnalyzer.PrefixAnalyzerId);
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var diagnostic in context.Diagnostics)
|
||||
{
|
||||
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
SyntaxNode node = root.FindNode(diagnosticSpan);
|
||||
|
||||
switch (diagnostic.Id)
|
||||
{
|
||||
case UnrealTypeAnalyzer.StructAnalyzerId:
|
||||
if (node is PropertyDeclarationSyntax propertyNode)
|
||||
{
|
||||
string name = propertyNode.Identifier.Text;
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: $"Convert '{name}' to field",
|
||||
createChangedDocument: c => ConvertPropertyToFieldAsync(context.Document, propertyNode, c),
|
||||
equivalenceKey: "ConvertToField"),
|
||||
diagnostic);
|
||||
}
|
||||
break;
|
||||
|
||||
case UnrealTypeAnalyzer.ClassAnalyzerId:
|
||||
if (node is VariableDeclaratorSyntax fieldNode)
|
||||
{
|
||||
string name = fieldNode.Identifier.Text;
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: $"Convert '{name}' to property",
|
||||
createChangedDocument: c => ConvertFieldToPropertyAsync(context.Document, fieldNode, c),
|
||||
equivalenceKey: "ConvertToProperty"),
|
||||
diagnostic);
|
||||
}
|
||||
break;
|
||||
|
||||
case UnrealTypeAnalyzer.PrefixAnalyzerId:
|
||||
if (node is BaseTypeDeclarationSyntax declaration)
|
||||
{
|
||||
var prefix = diagnostic.Properties["Prefix"];
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: $"Add prefix '{prefix}' to '{declaration.Identifier.Text}'",
|
||||
createChangedDocument: c => AddPrefixToDeclarationAsync(context.Document, declaration, prefix, c),
|
||||
equivalenceKey: "AddPrefix"),
|
||||
diagnostic);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Document> ConvertPropertyToFieldAsync(Document document, PropertyDeclarationSyntax propertyDeclaration, CancellationToken cancellationToken)
|
||||
{
|
||||
VariableDeclarationSyntax variableDeclaration = SyntaxFactory.VariableDeclaration(propertyDeclaration.Type)
|
||||
.AddVariables(SyntaxFactory.VariableDeclarator(propertyDeclaration.Identifier));
|
||||
|
||||
FieldDeclarationSyntax fieldDeclaration = SyntaxFactory.FieldDeclaration(variableDeclaration)
|
||||
.WithModifiers(propertyDeclaration.Modifiers)
|
||||
.WithAttributeLists(propertyDeclaration.AttributeLists)
|
||||
.WithTriviaFrom(propertyDeclaration);
|
||||
|
||||
SyntaxTriviaList leadingTrivia = propertyDeclaration.GetLeadingTrivia();
|
||||
SyntaxTriviaList trailingTrivia = propertyDeclaration.GetTrailingTrivia();
|
||||
|
||||
if (leadingTrivia.Any(t => t.IsKind(SyntaxKind.EndOfLineTrivia)))
|
||||
{
|
||||
var newLeadingTrivia = leadingTrivia.Where(t => !t.IsKind(SyntaxKind.EndOfLineTrivia));
|
||||
fieldDeclaration = fieldDeclaration.WithLeadingTrivia(newLeadingTrivia);
|
||||
}
|
||||
|
||||
fieldDeclaration = fieldDeclaration.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia);
|
||||
|
||||
SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
|
||||
SyntaxNode? newRoot = root.ReplaceNode(propertyDeclaration, fieldDeclaration);
|
||||
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
|
||||
private async Task<Document> ConvertFieldToPropertyAsync(Document document, VariableDeclaratorSyntax fieldDeclaration, CancellationToken cancellationToken)
|
||||
{
|
||||
if (fieldDeclaration.Parent is not VariableDeclarationSyntax parentFieldDeclaration)
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
if (parentFieldDeclaration.Parent is not FieldDeclarationSyntax fieldDecl)
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
PropertyDeclarationSyntax propertyDeclaration = SyntaxFactory.PropertyDeclaration(parentFieldDeclaration.Type, fieldDeclaration.Identifier)
|
||||
.AddModifiers(fieldDecl.Modifiers.ToArray())
|
||||
.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[]
|
||||
{
|
||||
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
|
||||
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
|
||||
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
|
||||
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
|
||||
})))
|
||||
.WithTriviaFrom(fieldDecl)
|
||||
.WithAttributeLists(fieldDecl.AttributeLists);
|
||||
|
||||
SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
|
||||
SyntaxNode? newRoot = root.ReplaceNode(fieldDecl, propertyDeclaration);
|
||||
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
|
||||
private async Task<Document> AddPrefixToDeclarationAsync(Document document, BaseTypeDeclarationSyntax declaration, string prefix,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SyntaxToken identifierToken = declaration.Identifier;
|
||||
string newName = prefix + identifierToken.Text;
|
||||
SyntaxToken newIdentifierToken = SyntaxFactory.Identifier(newName);
|
||||
|
||||
MemberDeclarationSyntax newDeclaration = declaration.WithIdentifier(newIdentifierToken)
|
||||
.WithTriviaFrom(declaration);
|
||||
|
||||
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
|
||||
SyntaxNode newRoot = root.ReplaceNode(declaration, newDeclaration);
|
||||
|
||||
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
|
||||
ISymbol symbol = semanticModel.GetDeclaredSymbol(declaration, cancellationToken);
|
||||
|
||||
Solution solution = document.Project.Solution;
|
||||
Solution newSolution = await Renamer
|
||||
.RenameSymbolAsync(solution, symbol, newName, solution.Options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return newSolution.GetDocument(document.Id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UStaticLambdaAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
id: "US0001",
|
||||
title: "Invalid UFunction lambda",
|
||||
messageFormat: "Static UFunction lambdas are not supported, since it has no backing UObject instance. Make this lambda an instance method or capture the instance by using instance fields/methods.",
|
||||
category: "UnrealSharp",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeLambda, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression);
|
||||
}
|
||||
|
||||
private void AnalyzeLambda(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
LambdaExpressionSyntax lambda = (LambdaExpressionSyntax) context.Node;
|
||||
|
||||
if (lambda.AttributeLists.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SemanticModel semanticModel = context.SemanticModel;
|
||||
|
||||
bool hasUFunction = lambda.AttributeLists
|
||||
.SelectMany(list => list.Attributes)
|
||||
.Any(attr => semanticModel.GetTypeInfo(attr).Type?.Name == "UFunctionAttribute");
|
||||
|
||||
if (!hasUFunction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DataFlowAnalysis? dataFlow = semanticModel.AnalyzeDataFlow(lambda);
|
||||
if (dataFlow != null && !dataFlow.Captured.Any())
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(Rule, lambda.GetLocation()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Editing;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UFunctionLambdaCodeFixProvider)), Shared]
|
||||
public class UFunctionLambdaCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create("US0001");
|
||||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
Diagnostic diagnostic = context.Diagnostics.First();
|
||||
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (root?.FindNode(diagnostic.Location.SourceSpan) is not LambdaExpressionSyntax lambda)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.RegisterCodeFix(Microsoft.CodeAnalysis.CodeActions.CodeAction.Create(
|
||||
title: "Convert Static Lambda to Instance Method",
|
||||
createChangedDocument: c => ConvertToInstanceMethod(context.Document, lambda, c),
|
||||
equivalenceKey: "ConvertToInstanceMethod"), diagnostic);
|
||||
}
|
||||
|
||||
private async Task<Document> ConvertToInstanceMethod(Document document, LambdaExpressionSyntax lambda, CancellationToken cancellationToken)
|
||||
{
|
||||
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken);
|
||||
DocumentEditor? editor = await DocumentEditor.CreateAsync(document, cancellationToken);
|
||||
|
||||
MethodDeclarationSyntax? containingMethod = lambda.FirstAncestorOrSelf<MethodDeclarationSyntax>();
|
||||
ClassDeclarationSyntax? containingClass = lambda.FirstAncestorOrSelf<ClassDeclarationSyntax>();
|
||||
|
||||
if (containingMethod == null || containingClass == null || semanticModel == null)
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
BlockSyntax lambdaBody = lambda.Body as BlockSyntax
|
||||
?? SyntaxFactory.Block(SyntaxFactory.ExpressionStatement((ExpressionSyntax)lambda.Body));
|
||||
|
||||
SeparatedSyntaxList<ParameterSyntax> parameters = lambda switch
|
||||
{
|
||||
SimpleLambdaExpressionSyntax simple => SyntaxFactory.SingletonSeparatedList(WithInferredType(simple.Parameter, semanticModel, cancellationToken)),
|
||||
ParenthesizedLambdaExpressionSyntax paren => SyntaxFactory.SeparatedList(paren.ParameterList.Parameters.Select(p => WithInferredType(p, semanticModel, cancellationToken))),
|
||||
_ => default
|
||||
};
|
||||
|
||||
string methodName = AnalyzerStatics.GenerateUniqueMethodName(containingClass, "InstanceMethod");
|
||||
editor.ReplaceNode(lambda, SyntaxFactory.IdentifierName(methodName));
|
||||
|
||||
MethodDeclarationSyntax methodDecl = SyntaxFactory.MethodDeclaration(
|
||||
SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), methodName)
|
||||
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword))
|
||||
.WithAttributeLists(lambda.AttributeLists)
|
||||
.WithParameterList(SyntaxFactory.ParameterList(parameters))
|
||||
.WithBody(lambdaBody);
|
||||
|
||||
editor.AddMember(containingClass, methodDecl);
|
||||
|
||||
Document? changedDocument = editor.GetChangedDocument();
|
||||
return await Formatter.FormatAsync(changedDocument, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static ParameterSyntax WithInferredType(ParameterSyntax parameter, SemanticModel model, CancellationToken token)
|
||||
{
|
||||
IParameterSymbol? symbol = model.GetDeclaredSymbol(parameter, token);
|
||||
|
||||
if (symbol == null)
|
||||
{
|
||||
return parameter.WithType(SyntaxFactory.IdentifierName("object"));
|
||||
}
|
||||
|
||||
SymbolDisplayFormat displayFormat = new SymbolDisplayFormat(
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes
|
||||
);
|
||||
|
||||
string typeName = symbol.Type.ToDisplayString(displayFormat);
|
||||
return parameter.WithType(SyntaxFactory.ParseTypeName(typeName));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UEnumAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
UEnumIsByteEnumRule
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UEnumIsByteEnumRule = new(
|
||||
id: "US0002",
|
||||
title: "UnrealSharp UEnumIsByteEnum Analyzer",
|
||||
messageFormat: "{0} is a UEnum, which should have a underlying type of byte",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures UEnum underlying type is byte."
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.RegisterSymbolAction(Test, SymbolKind.NamedType);
|
||||
}
|
||||
|
||||
private static void Test(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not INamedTypeSymbol namedTypeSymbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isUEnum = namedTypeSymbol.TypeKind == TypeKind.Enum && AnalyzerStatics.HasAttribute(namedTypeSymbol, AnalyzerStatics.UEnumAttribute);
|
||||
if (isUEnum && !IsByteEnum(namedTypeSymbol) && !AnalyzerStatics.HasAttribute(namedTypeSymbol, AnalyzerStatics.GeneratedTypeAttribute))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UEnumIsByteEnumRule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsByteEnum(INamedTypeSymbol symbol)
|
||||
{
|
||||
return symbol.EnumUnderlyingType?.SpecialType == SpecialType.System_Byte;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UInterfaceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
UInterfacePropertyTypeRule,
|
||||
UInterfaceFunctionParameterTypeRule
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UInterfacePropertyTypeRule = new(
|
||||
id: "US0003",
|
||||
title: "UnrealSharp UInterface UProperty Analyzer",
|
||||
messageFormat: "{0} is a UProperty with Interface type, which should has UInterface attribute",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures UProperty type has a UInterface attribute."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UInterfaceFunctionParameterTypeRule = new(
|
||||
id: "US0004",
|
||||
title: "UnrealSharp UInterface function parameter Analyzer",
|
||||
messageFormat: "{0} is UFunction parameter with Interface type, which should has UInterface attribute",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures interface type has a UInterface attribute."
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.RegisterSymbolAction(AnalyzeProperty, SymbolKind.Property);
|
||||
context.RegisterSymbolAction(AnalyzeFunctionParameter, SymbolKind.Parameter);
|
||||
}
|
||||
|
||||
private static void AnalyzeProperty(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not IPropertySymbol propertySymbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AnalyzerStatics.HasAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isInterfaceType = propertySymbol.Type.TypeKind == TypeKind.Interface;
|
||||
if (!isInterfaceType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hasUInterfaceAttribute = AnalyzerStatics.HasAttribute(propertySymbol.Type, AnalyzerStatics.UInterfaceAttribute);
|
||||
|
||||
if (!hasUInterfaceAttribute && !AnalyzerStatics.IsContainerInterface(propertySymbol.Type))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UInterfacePropertyTypeRule, propertySymbol.Locations[0], propertySymbol.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeFunctionParameter(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is not IParameterSymbol parameterSymbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isMethodParameter = parameterSymbol.ContainingSymbol.Kind == SymbolKind.Method;
|
||||
if (!isMethodParameter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isUFunction = AnalyzerStatics.HasAttribute(context.Symbol.ContainingSymbol, AnalyzerStatics.UFunctionAttribute);
|
||||
if (!isUFunction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isInterfaceType = parameterSymbol.Type.TypeKind == TypeKind.Interface;
|
||||
if (!isInterfaceType)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hasUInterfaceAttribute = AnalyzerStatics.HasAttribute(parameterSymbol.Type, AnalyzerStatics.UInterfaceAttribute);
|
||||
if (!hasUInterfaceAttribute && !AnalyzerStatics.IsContainerInterface(parameterSymbol.Type))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UInterfaceFunctionParameterTypeRule, parameterSymbol.Locations[0], parameterSymbol.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UObjectCreationAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
UObjectCreationRule,
|
||||
AActorCreationRule,
|
||||
UUserWidgetCreationRule,
|
||||
UActorComponentCreationRule,
|
||||
USceneComponentCreationRule
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UObjectCreationRule = new(
|
||||
id: "US0011",
|
||||
title: "UnrealSharp UObject creation Analyzer",
|
||||
messageFormat: "{0} is a UObject, which should be created by calling the method NewObject<T>()",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures UObject instantiated by using NewObject<T>() method."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor AActorCreationRule = new(
|
||||
id: "US0010",
|
||||
title: "UnrealSharp AActor creation Analyzer",
|
||||
messageFormat: "{0} is a AActor, which should be created by calling the method SpawnActor<T>()",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures AActor instantiated by using SpawnActor<T>() method."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UUserWidgetCreationRule = new(
|
||||
id: "US0009",
|
||||
title: "UnrealSharp UUserWidget creation Analyzer",
|
||||
messageFormat: "{0} is a UUserWidget, which should be created by calling the method CreateWidget<T>()",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures UUserWidget instantiated by using CreateWidget<T>() method."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor UActorComponentCreationRule = new(
|
||||
id: "US0008",
|
||||
title: "UnrealSharp UActorComponent creation Analyzer",
|
||||
messageFormat: "{0} is a UActorComponent, which should be created by calling the method AddComponentByClass<T>()",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures UActorComponent instantiated by using AddComponentByClass<T>() method."
|
||||
);
|
||||
|
||||
private static readonly DiagnosticDescriptor USceneComponentCreationRule = new(
|
||||
id: "US0007",
|
||||
title: "UnrealSharp USceneComponent creation Analyzer",
|
||||
messageFormat: "{0} is a USceneComponent, which should be created by calling the method AddComponentByClass<T>()",
|
||||
RuleCategory.Category,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Ensures USceneComponent instantiated by using AddComponentByClass<T>() method."
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
|
||||
context.RegisterOperationAction(AnalyzeUObjectCreation, OperationKind.ObjectCreation);
|
||||
}
|
||||
|
||||
//check new <UObject> syntax
|
||||
private static void AnalyzeUObjectCreation(OperationAnalysisContext context)
|
||||
{
|
||||
if (context.Operation is not IObjectCreationOperation creationOperation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (creationOperation.Type is not INamedTypeSymbol type)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isNewKeywordOperation = AnalyzerStatics.IsNewKeywordInstancingOperation(creationOperation, out var newKeywordLocation);
|
||||
if (!isNewKeywordOperation) return;
|
||||
|
||||
var rule = GetRule(type);
|
||||
if (rule is null) return;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(rule, newKeywordLocation, type.Name));
|
||||
|
||||
}
|
||||
|
||||
private static DiagnosticDescriptor? GetRule(INamedTypeSymbol type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
_ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.USceneComponent) => USceneComponentCreationRule,
|
||||
_ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UActorComponent) => UActorComponentCreationRule,
|
||||
_ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UUserWidget) => UUserWidgetCreationRule,
|
||||
_ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.AActor) => AActorCreationRule,
|
||||
_ when AnalyzerStatics.InheritsFrom(type, AnalyzerStatics.UObject) => UObjectCreationRule,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeAnalyzers;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UnrealTypeAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
|
||||
PrefixRule,
|
||||
ClassRule
|
||||
);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSymbolAction(AnalyzeType, SymbolKind.NamedType);
|
||||
context.RegisterSymbolAction(AnalyzeClassFields, SymbolKind.Field);
|
||||
}
|
||||
|
||||
public const string StructAnalyzerId = "US0005";
|
||||
public const string ClassAnalyzerId = "US0006";
|
||||
private static readonly LocalizableString StructAnalyzerTitle = "UnrealSharp Struct Field Analyzer";
|
||||
private static readonly LocalizableString ClassAnalyzerTitle = "UnrealSharp Class Field Analyzer";
|
||||
private static readonly LocalizableString ClassAnalyzerMessageFormat = "{0} is a UProperty and a field, which is not allowed in classes. UProperties in classes must be properties.";
|
||||
private static readonly LocalizableString StructAnalyzerDescription = "Ensures UProperties in structs are fields.";
|
||||
private static readonly LocalizableString ClassAnalyzerDescription = "Ensures UProperties in classes are properties.";
|
||||
|
||||
private static readonly DiagnosticDescriptor ClassRule = new(ClassAnalyzerId, ClassAnalyzerTitle, ClassAnalyzerMessageFormat, RuleCategory.Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: ClassAnalyzerDescription);
|
||||
|
||||
private static void AnalyzeFields(SymbolAnalysisContext context, TypeKind typeKind, string requiredAttribute, DiagnosticDescriptor rule)
|
||||
{
|
||||
ISymbol symbol = context.Symbol;
|
||||
INamedTypeSymbol type = symbol.ContainingType;
|
||||
|
||||
if (type.TypeKind != typeKind && !AnalyzerStatics.HasAttribute(type, requiredAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UPropertyAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var diagnostic = Diagnostic.Create(rule, symbol.Locations[0], symbol.Name);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static void AnalyzeClassFields(SymbolAnalysisContext context)
|
||||
{
|
||||
if (context.Symbol is IFieldSymbol)
|
||||
{
|
||||
AnalyzeFields(context, TypeKind.Class, AnalyzerStatics.UClassAttribute, ClassRule);
|
||||
}
|
||||
}
|
||||
|
||||
public const string PrefixAnalyzerId = "PrefixAnalyzer";
|
||||
private static readonly LocalizableString PrefixAnalyzerTitle = "UnrealSharp Prefix Analyzer";
|
||||
private static readonly LocalizableString PrefixAnalyzerMessageFormat = "{0} '{1}' is exposed to Unreal Engine and should have prefix '{2}'";
|
||||
private static readonly LocalizableString PrefixAnalyzerDescription = "Ensures types have appropriate prefixes.";
|
||||
private static readonly DiagnosticDescriptor PrefixRule = new(PrefixAnalyzerId, PrefixAnalyzerTitle, PrefixAnalyzerMessageFormat, RuleCategory.Naming, DiagnosticSeverity.Error, isEnabledByDefault: true, description: PrefixAnalyzerDescription);
|
||||
|
||||
private static void AnalyzeType(SymbolAnalysisContext context)
|
||||
{
|
||||
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
|
||||
string prefix = null;
|
||||
|
||||
// These types are generated by the script generator, and already have the correct prefix
|
||||
if (AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.GeneratedTypeAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (symbol.TypeKind == TypeKind.Struct && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UStructAttribute))
|
||||
{
|
||||
prefix = "F";
|
||||
}
|
||||
else if (symbol.TypeKind == TypeKind.Enum && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UEnumAttribute))
|
||||
{
|
||||
prefix = "E";
|
||||
}
|
||||
else if (symbol.TypeKind == TypeKind.Class)
|
||||
{
|
||||
if (!AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UClassAttribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AnalyzerStatics.InheritsFrom(symbol, AnalyzerStatics.AActor))
|
||||
{
|
||||
prefix = "A";
|
||||
}
|
||||
else if (AnalyzerStatics.InheritsFrom(symbol, AnalyzerStatics.UObject))
|
||||
{
|
||||
prefix = "U";
|
||||
}
|
||||
}
|
||||
else if (symbol.TypeKind == TypeKind.Interface && AnalyzerStatics.HasAttribute(symbol, AnalyzerStatics.UInterfaceAttribute))
|
||||
{
|
||||
prefix = "I";
|
||||
}
|
||||
|
||||
if (prefix == null || symbol.Name.StartsWith(prefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> properties = new Dictionary<string, string>
|
||||
{
|
||||
{ "Prefix", prefix }
|
||||
};
|
||||
|
||||
Diagnostic diagnostic = Diagnostic.Create(PrefixRule, symbol.Locations[0], properties.ToImmutableDictionary(), symbol.TypeKind.ToString(), symbol.Name, prefix);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.CodeSuppressors;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UPropertyReferenceTypeSuppressor : DiagnosticSuppressor
|
||||
{
|
||||
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [DefaultComponentNullableRule];
|
||||
|
||||
private static readonly SuppressionDescriptor DefaultComponentNullableRule = new(
|
||||
"NullableUPropertySuppressorId",
|
||||
"CS8618",
|
||||
"UProperties on UClasses are automatically instantiated."
|
||||
);
|
||||
|
||||
public override void ReportSuppressions(SuppressionAnalysisContext context)
|
||||
{
|
||||
foreach (var diagnostic in context.ReportedDiagnostics)
|
||||
{
|
||||
var location = diagnostic.Location;
|
||||
var syntaxTree = location.SourceTree;
|
||||
if (syntaxTree is null) continue;
|
||||
|
||||
var root = syntaxTree.GetRoot(context.CancellationToken);
|
||||
var textSpan = location.SourceSpan;
|
||||
var node = root.FindNode(textSpan);
|
||||
|
||||
if (node is not PropertyDeclarationSyntax propertyNode || propertyNode.AttributeLists.Count == 0) continue;
|
||||
|
||||
var semanticModel = context.GetSemanticModel(syntaxTree);
|
||||
|
||||
if (IsDefaultComponentProperty(semanticModel, propertyNode, context.CancellationToken))
|
||||
{
|
||||
context.ReportSuppression(Suppression.Create(DefaultComponentNullableRule, diagnostic));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDefaultComponentProperty(
|
||||
SemanticModel semanticModel,
|
||||
PropertyDeclarationSyntax propertyNode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var symbol = semanticModel.GetDeclaredSymbol(propertyNode, cancellationToken);
|
||||
if (symbol is not IPropertySymbol propertySymbol) return false;
|
||||
|
||||
return AnalyzerStatics.TryGetAttribute(propertySymbol, AnalyzerStatics.UPropertyAttribute, out _) &&
|
||||
AnalyzerStatics.HasAttribute(propertySymbol.ContainingType, AnalyzerStatics.UClassAttribute);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
[Generator]
|
||||
public class CustomLogSourceGenerator : IIncrementalGenerator
|
||||
{
|
||||
private readonly struct ClassLogInfo
|
||||
{
|
||||
public readonly INamedTypeSymbol ClassSymbol;
|
||||
public readonly string LogVerbosity;
|
||||
public ClassLogInfo(INamedTypeSymbol classSymbol, string logVerbosity)
|
||||
{
|
||||
ClassSymbol = classSymbol;
|
||||
LogVerbosity = logVerbosity;
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var classLogInfos = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,
|
||||
static (syntaxContext, _) => GetClassLogInfos(syntaxContext))
|
||||
.SelectMany(static (infos, _) => infos)
|
||||
.Where(static info => info.ClassSymbol is not null);
|
||||
|
||||
context.RegisterSourceOutput(classLogInfos, static (spc, info) =>
|
||||
{
|
||||
string source = GenerateLoggerClass(info.ClassSymbol, info.ClassSymbol.Name, info.LogVerbosity);
|
||||
spc.AddSource($"{info.ClassSymbol.Name}_CustomLog.generated.cs", SourceText.From(source, Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
|
||||
private static IEnumerable<ClassLogInfo> GetClassLogInfos(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
||||
{
|
||||
return Array.Empty<ClassLogInfo>();
|
||||
}
|
||||
|
||||
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
|
||||
{
|
||||
return Array.Empty<ClassLogInfo>();
|
||||
}
|
||||
|
||||
List<ClassLogInfo> list = new();
|
||||
|
||||
foreach (var attributeList in classDeclaration.AttributeLists)
|
||||
{
|
||||
foreach (var attribute in attributeList.Attributes)
|
||||
{
|
||||
var attributeName = attribute.Name.ToString();
|
||||
if (attributeName is not ("CustomLog" or "CustomLogAttribute"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var firstArgument = attribute.ArgumentList?.Arguments.FirstOrDefault();
|
||||
string logVerbosity = firstArgument != null ? firstArgument.Expression.ToString() : "ELogVerbosity.Display";
|
||||
list.Add(new ClassLogInfo(classSymbol, logVerbosity));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static string GenerateLoggerClass(INamedTypeSymbol classSymbol, string logFieldName, string logVerbosity)
|
||||
{
|
||||
string namespaceName = classSymbol.ContainingNamespace.IsGlobalNamespace
|
||||
? string.Empty
|
||||
: classSymbol.ContainingNamespace.ToDisplayString();
|
||||
|
||||
string className = classSymbol.Name;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("using UnrealSharp.Log;");
|
||||
|
||||
if (!string.IsNullOrEmpty(namespaceName))
|
||||
{
|
||||
builder.AppendLine($"namespace {namespaceName};");
|
||||
}
|
||||
|
||||
builder.AppendLine($"public partial class {className}");
|
||||
builder.AppendLine("{");
|
||||
builder.AppendLine($" public static void Log(string message) => UnrealLogger.Log(\"{logFieldName}\", message, {logVerbosity});");
|
||||
builder.AppendLine($" public static void LogWarning(string message) => UnrealLogger.LogWarning(\"{logFieldName}\", message);");
|
||||
builder.AppendLine($" public static void LogError(string message) => UnrealLogger.LogError(\"{logFieldName}\", message);");
|
||||
builder.AppendLine($" public static void LogFatal(string message) => UnrealLogger.LogFatal(\"{logFieldName}\", message);");
|
||||
builder.AppendLine("}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.DelegateGenerator;
|
||||
|
||||
public abstract class DelegateBuilder
|
||||
{
|
||||
public abstract void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker);
|
||||
|
||||
protected void GenerateGetInvoker(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol)
|
||||
{
|
||||
stringBuilder.AppendLine($" protected override {delegateSymbol} GetInvoker()");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" return Invoker;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
protected void GenerateInvoke(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol)
|
||||
{
|
||||
if (delegateSymbol.DelegateInvokeMethod == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (delegateSymbol.DelegateInvokeMethod.Parameters.IsEmpty)
|
||||
{
|
||||
stringBuilder.AppendLine($" protected void Invoker()");
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append($" protected void Invoker(");
|
||||
stringBuilder.Append(string.Join(", ", delegateSymbol.DelegateInvokeMethod.Parameters.Select(x => $"{DelegateWrapperGenerator.GetRefKindKeyword(x)}{x.Type} {x.Name}")));
|
||||
stringBuilder.Append(")");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" ProcessDelegate(IntPtr.Zero);");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public enum DelegateType
|
||||
{
|
||||
Multicast,
|
||||
Single,
|
||||
}
|
||||
@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.DelegateGenerator;
|
||||
|
||||
[Generator]
|
||||
public class DelegateWrapperGenerator : IIncrementalGenerator
|
||||
{
|
||||
private sealed class DelegateGenerationInfo
|
||||
{
|
||||
public string NamespaceName { get; }
|
||||
public string DelegateName { get; }
|
||||
public INamedTypeSymbol DelegateSymbol { get; }
|
||||
public bool GenerateInvoker { get; }
|
||||
public DelegateType DelegateType { get; }
|
||||
public string BaseTypeName { get; }
|
||||
public bool NullableAwareable { get; }
|
||||
public DelegateGenerationInfo(string namespaceName, string delegateName, INamedTypeSymbol delegateSymbol, bool generateInvoker, DelegateType delegateType, string baseTypeName, bool nullableAwareable)
|
||||
{
|
||||
NamespaceName = namespaceName;
|
||||
DelegateName = delegateName;
|
||||
DelegateSymbol = delegateSymbol;
|
||||
GenerateInvoker = generateInvoker;
|
||||
DelegateType = delegateType;
|
||||
BaseTypeName = baseTypeName;
|
||||
NullableAwareable = nullableAwareable;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { BaseList: not null } || syntaxNode is DelegateDeclarationSyntax,
|
||||
static (syntaxContext, _) => GetInfoOrNull(syntaxContext))
|
||||
.Where(static info => info is not null)
|
||||
.Select(static (info, _) => info!);
|
||||
|
||||
context.RegisterSourceOutput(candidates, static (spc, info) => Generate(spc, info));
|
||||
}
|
||||
|
||||
private static DelegateGenerationInfo? GetInfoOrNull(GeneratorSyntaxContext context)
|
||||
{
|
||||
// Exclude members with [Binding]
|
||||
if (context.Node is MemberDeclarationSyntax m && AnalyzerStatics.HasAttribute(m, "Binding"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
INamedTypeSymbol? symbol;
|
||||
INamedTypeSymbol? delegateSymbol;
|
||||
string delegateName;
|
||||
bool generateInvoker = true;
|
||||
DelegateType delegateType;
|
||||
string baseTypeName;
|
||||
|
||||
if (context.Node is ClassDeclarationSyntax classDecl)
|
||||
{
|
||||
// Must derive from *Delegate
|
||||
if (classDecl.BaseList == null || !classDecl.BaseList.Types.Any(bt => bt.Type.ToString().Contains("MulticastDelegate") || bt.Type.ToString().Contains("Delegate")))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
|
||||
if (symbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbol.IsGenericType || AnalyzerStatics.HasAttribute(symbol, "UnmanagedFunctionPointerAttribute"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
delegateName = classDecl.Identifier.ValueText;
|
||||
delegateSymbol = symbol.BaseType?.TypeArguments.FirstOrDefault() as INamedTypeSymbol;
|
||||
if (delegateSymbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
generateInvoker = !symbol.GetMembers().Any(x => x.Name == "Invoker");
|
||||
}
|
||||
else if (context.Node is DelegateDeclarationSyntax delegateDecl)
|
||||
{
|
||||
if (AnalyzerStatics.HasAttribute(delegateDecl, "GeneratedType"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(delegateDecl) as INamedTypeSymbol;
|
||||
if (symbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (symbol.IsGenericType || AnalyzerStatics.HasAttribute(symbol, "UnmanagedFunctionPointerAttribute"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
delegateName = "U" + delegateDecl.Identifier.ValueText;
|
||||
delegateSymbol = symbol; // Underlying delegate is the symbol itself
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (AnalyzerStatics.HasAttribute(delegateSymbol, AnalyzerStatics.USingleDelegateAttribute))
|
||||
{
|
||||
baseTypeName = "Delegate";
|
||||
delegateType = DelegateType.Single;
|
||||
}
|
||||
else if (AnalyzerStatics.HasAttribute(delegateSymbol, AnalyzerStatics.UMultiDelegateAttribute))
|
||||
{
|
||||
baseTypeName = "MulticastDelegate";
|
||||
delegateType = DelegateType.Multicast;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null; // Not a recognized Unreal delegate wrapper
|
||||
}
|
||||
|
||||
string namespaceName = symbol.ContainingNamespace?.ToDisplayString() ?? "Global";
|
||||
|
||||
return new DelegateGenerationInfo(namespaceName, delegateName, delegateSymbol, generateInvoker, delegateType, baseTypeName,
|
||||
context.SemanticModel.GetNullableContext(context.Node.Span.Start).HasFlag(NullableContext.AnnotationsEnabled));
|
||||
}
|
||||
|
||||
private static void Generate(SourceProductionContext context, DelegateGenerationInfo info)
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
if (info.NullableAwareable)
|
||||
{
|
||||
stringBuilder.AppendLine("#nullable enable");
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.AppendLine("#nullable disable");
|
||||
}
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine("using UnrealSharp;");
|
||||
stringBuilder.AppendLine("using UnrealSharp.Interop;");
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.AppendLine($"namespace {info.NamespaceName};");
|
||||
stringBuilder.AppendLine();
|
||||
|
||||
DelegateBuilder builder = info.DelegateType == DelegateType.Multicast
|
||||
? new MulticastDelegateBuilder()
|
||||
: new SingleDelegateBuilder();
|
||||
|
||||
stringBuilder.AppendLine($"public partial class {info.DelegateName} : {info.BaseTypeName}<{info.DelegateSymbol}>");
|
||||
stringBuilder.AppendLine("{");
|
||||
builder.StartBuilding(stringBuilder, info.DelegateSymbol, info.DelegateName, info.GenerateInvoker);
|
||||
stringBuilder.AppendLine("}");
|
||||
stringBuilder.AppendLine();
|
||||
GenerateDelegateExtensionsClass(stringBuilder, info.DelegateSymbol, info.DelegateName, info.DelegateType);
|
||||
|
||||
context.AddSource($"{info.NamespaceName}.{info.DelegateName}.generated.cs", SourceText.From(stringBuilder.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
private static void GenerateDelegateExtensionsClass(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string delegateName, DelegateType delegateType)
|
||||
{
|
||||
stringBuilder.AppendLine($"public static class {delegateName}Extensions");
|
||||
stringBuilder.AppendLine("{");
|
||||
|
||||
var parametersList = delegateSymbol.DelegateInvokeMethod!.Parameters.ToList();
|
||||
|
||||
string args = parametersList.Any()
|
||||
? string.Join(", ", parametersList.Select(x => $"{GetRefKindKeyword(x)}{x.Type} {x.Name}"))
|
||||
: string.Empty;
|
||||
|
||||
string parameters = parametersList.Any()
|
||||
? string.Join(", ", parametersList.Select(x => $"{GetRefKindKeyword(x)}{x.Name}"))
|
||||
: string.Empty;
|
||||
|
||||
string delegateTypeString = delegateType == DelegateType.Multicast ? "TMulticastDelegate" : "TDelegate";
|
||||
|
||||
stringBuilder.AppendLine($" public static void Invoke(this {delegateTypeString}<{delegateSymbol}> @delegate{(args.Any() ? $", {args}" : string.Empty)})");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine($" @delegate.InnerDelegate.Invoke({parameters});");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine("}");
|
||||
}
|
||||
|
||||
internal static string GetRefKindKeyword(IParameterSymbol x)
|
||||
{
|
||||
return x.RefKind switch
|
||||
{
|
||||
RefKind.RefReadOnlyParameter => "in ",
|
||||
RefKind.In => "in ",
|
||||
RefKind.Ref => "ref ",
|
||||
RefKind.Out => "out ",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using UnrealSharp.SourceGenerators.DelegateGenerator;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public class MulticastDelegateBuilder : DelegateBuilder
|
||||
{
|
||||
public override void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker)
|
||||
{
|
||||
GenerateAddOperator(stringBuilder, delegateSymbol, className);
|
||||
GenerateGetInvoker(stringBuilder, delegateSymbol);
|
||||
GenerateRemoveOperator(stringBuilder, delegateSymbol, className);
|
||||
|
||||
//Check if the class has an Invoker method already
|
||||
if (generateInvoker)
|
||||
{
|
||||
GenerateInvoke(stringBuilder, delegateSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateAddOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className)
|
||||
{
|
||||
stringBuilder.AppendLine($" public static {className} operator +({className} thisDelegate, {delegateSymbol.Name} handler)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" thisDelegate.Add(handler);");
|
||||
stringBuilder.AppendLine(" return thisDelegate;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
void GenerateRemoveOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className)
|
||||
{
|
||||
stringBuilder.AppendLine($" public static {className} operator -({className} thisDelegate, {delegateSymbol.Name} handler)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" thisDelegate.Remove(handler);");
|
||||
stringBuilder.AppendLine(" return thisDelegate;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using UnrealSharp.SourceGenerators.DelegateGenerator;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public class SingleDelegateBuilder : DelegateBuilder
|
||||
{
|
||||
public override void StartBuilding(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className, bool generateInvoker)
|
||||
{
|
||||
GenerateAddOperator(stringBuilder, delegateSymbol, className);
|
||||
|
||||
GenerateGetInvoker(stringBuilder, delegateSymbol);
|
||||
|
||||
GenerateRemoveOperator(stringBuilder, delegateSymbol, className);
|
||||
|
||||
GenerateConstructors(stringBuilder, className);
|
||||
|
||||
//Check if the class has an Invoker method already
|
||||
if (generateInvoker)
|
||||
{
|
||||
GenerateInvoke(stringBuilder, delegateSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
void GenerateConstructors(StringBuilder stringBuilder, string className)
|
||||
{
|
||||
stringBuilder.AppendLine($" public {className}() : base()");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
|
||||
stringBuilder.AppendLine($" public {className}(DelegateData data) : base(data)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
|
||||
stringBuilder.AppendLine($" public {className}(UnrealSharp.CoreUObject.UObject targetObject, FName functionName) : base(targetObject, functionName)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
void GenerateAddOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className)
|
||||
{
|
||||
stringBuilder.AppendLine($" public static {className} operator +({className} thisDelegate, {delegateSymbol.Name} handler)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" thisDelegate.Add(handler);");
|
||||
stringBuilder.AppendLine(" return thisDelegate;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
void GenerateRemoveOperator(StringBuilder stringBuilder, INamedTypeSymbol delegateSymbol, string className)
|
||||
{
|
||||
stringBuilder.AppendLine($" public static {className} operator -({className} thisDelegate, {delegateSymbol.Name} handler)");
|
||||
stringBuilder.AppendLine(" {");
|
||||
stringBuilder.AppendLine(" thisDelegate.Remove(handler);");
|
||||
stringBuilder.AppendLine(" return thisDelegate;");
|
||||
stringBuilder.AppendLine(" }");
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,336 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public struct ParameterInfo
|
||||
{
|
||||
public DelegateParameterInfo Parameter { get; set; }
|
||||
}
|
||||
|
||||
[Generator]
|
||||
public class NativeCallbacksWrapperGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,
|
||||
static (syntaxContext, _) => GetClassInfoOrNull(syntaxContext));
|
||||
|
||||
var classAndCompilation = classDeclarations.Combine(context.CompilationProvider);
|
||||
|
||||
context.RegisterSourceOutput(classAndCompilation, (spc, pair) =>
|
||||
{
|
||||
var maybeClassInfo = pair.Left; // ClassInfo?
|
||||
var compilation = pair.Right;
|
||||
if (!maybeClassInfo.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
GenerateForClass(spc, compilation, maybeClassInfo.Value);
|
||||
});
|
||||
}
|
||||
|
||||
private static void GenerateForClass(SourceProductionContext context, Compilation compilation, ClassInfo classInfo)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(classInfo.ClassDeclaration.SyntaxTree);
|
||||
var sourceBuilder = new StringBuilder();
|
||||
|
||||
HashSet<string> namespaces = [];
|
||||
foreach (DelegateInfo delegateInfo in classInfo.Delegates)
|
||||
{
|
||||
foreach (var parameter in delegateInfo.ParametersAndReturnValue)
|
||||
{
|
||||
var typeInfo = model.GetTypeInfo(parameter.Type);
|
||||
var typeSymbol = typeInfo.Type;
|
||||
|
||||
if (typeSymbol == null || typeSymbol.ContainingNamespace == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeSymbol is INamedTypeSymbol nts && nts.IsGenericType)
|
||||
{
|
||||
namespaces.UnionWith(nts.TypeArguments.Where(t => t.ContainingNamespace != null).Select(t => t.ContainingNamespace!.ToDisplayString()));
|
||||
}
|
||||
|
||||
namespaces.Add(typeSymbol.ContainingNamespace.ToDisplayString());
|
||||
}
|
||||
}
|
||||
|
||||
if (classInfo.NullableAwareable)
|
||||
{
|
||||
sourceBuilder.AppendLine("#nullable enable");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.AppendLine("#nullable disable");
|
||||
}
|
||||
sourceBuilder.AppendLine("#pragma warning disable CS8500, CS0414");
|
||||
sourceBuilder.AppendLine();
|
||||
|
||||
foreach (string? ns in namespaces)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns)) continue;
|
||||
sourceBuilder.AppendLine($"using {ns};");
|
||||
}
|
||||
|
||||
sourceBuilder.AppendLine();
|
||||
sourceBuilder.AppendLine($"namespace {classInfo.Namespace}");
|
||||
sourceBuilder.AppendLine("{");
|
||||
sourceBuilder.AppendLine($" public static unsafe partial class {classInfo.Name}");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
|
||||
sourceBuilder.AppendLine(" static " + classInfo.Name + "()");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
|
||||
foreach (DelegateInfo delegateInfo in classInfo.Delegates)
|
||||
{
|
||||
string delegateName = delegateInfo.Name;
|
||||
|
||||
string totalSizeDelegateName = delegateName + "TotalSize";
|
||||
if (!delegateInfo.HasReturnValue && delegateInfo.Parameters.Count == 0)
|
||||
{
|
||||
sourceBuilder.AppendLine($" int {totalSizeDelegateName} = 0;");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.Append($" int {totalSizeDelegateName} = ");
|
||||
|
||||
void AppendSizeOf(DelegateParameterInfo param)
|
||||
{
|
||||
string typeFullName = param.Type.GetAnnotatedTypeName(model) ?? param.Type.ToString();
|
||||
|
||||
if (param.IsOutParameter || param.IsRefParameter)
|
||||
{
|
||||
sourceBuilder.Append($"IntPtr.Size");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.Append($"sizeof({typeFullName})");
|
||||
}
|
||||
}
|
||||
|
||||
List<DelegateParameterInfo> parameters = delegateInfo.ParametersAndReturnValue;
|
||||
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
AppendSizeOf(parameters[i]);
|
||||
|
||||
if (i != parameters.Count - 1)
|
||||
{
|
||||
sourceBuilder.Append(" + ");
|
||||
}
|
||||
}
|
||||
|
||||
sourceBuilder.AppendLine(";");
|
||||
}
|
||||
|
||||
string funcPtrName = delegateName + "FuncPtr";
|
||||
sourceBuilder.AppendLine($" IntPtr {funcPtrName} = UnrealSharp.Binds.NativeBinds.TryGetBoundFunction(\"{classInfo.Name}\", \"{delegateInfo.Name}\", {totalSizeDelegateName});");
|
||||
sourceBuilder.Append($" {delegateName} = (delegate* unmanaged<");
|
||||
sourceBuilder.Append(string.Join(", ", delegateInfo.Parameters.Select(p =>
|
||||
{
|
||||
string prefix = p.IsOutParameter ? "out " : p.IsRefParameter ? "ref " : string.Empty;
|
||||
return prefix + (p.Type.GetAnnotatedTypeName(model) ?? p.Type.ToString());
|
||||
})));
|
||||
|
||||
if (delegateInfo.Parameters.Count > 0)
|
||||
{
|
||||
sourceBuilder.Append(", ");
|
||||
}
|
||||
|
||||
sourceBuilder.Append(delegateInfo.ReturnValue.Type.GetAnnotatedTypeName(model) ?? delegateInfo.ReturnValue.Type.ToString());
|
||||
|
||||
sourceBuilder.Append($">){funcPtrName};");
|
||||
sourceBuilder.AppendLine();
|
||||
}
|
||||
|
||||
sourceBuilder.AppendLine(" }");
|
||||
|
||||
foreach (DelegateInfo delegateInfo in classInfo.Delegates)
|
||||
{
|
||||
string returnTypeFullName = delegateInfo.ReturnValue.Type.GetAnnotatedTypeName(model) ?? delegateInfo.ReturnValue.Type.ToString();
|
||||
sourceBuilder.AppendLine($" [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]");
|
||||
sourceBuilder.Append($" public static {returnTypeFullName} Call{delegateInfo.Name}(");
|
||||
|
||||
bool firstParameter = true;
|
||||
foreach (DelegateParameterInfo parameter in delegateInfo.Parameters)
|
||||
{
|
||||
if (!firstParameter)
|
||||
{
|
||||
sourceBuilder.Append(", ");
|
||||
}
|
||||
|
||||
firstParameter = false;
|
||||
|
||||
if (parameter.IsOutParameter)
|
||||
{
|
||||
sourceBuilder.Append("out ");
|
||||
}
|
||||
|
||||
if (parameter.IsRefParameter)
|
||||
{
|
||||
sourceBuilder.Append("ref ");
|
||||
}
|
||||
|
||||
string typeFullName = parameter.Type.GetAnnotatedTypeName(model) ?? parameter.Type.ToString();
|
||||
sourceBuilder.Append($"{typeFullName} {parameter.Name}");
|
||||
}
|
||||
|
||||
sourceBuilder.AppendLine(")");
|
||||
sourceBuilder.AppendLine(" {");
|
||||
|
||||
string delegateName = delegateInfo.Name;
|
||||
|
||||
if (delegateInfo.ReturnValue.Type.ToString() != "void")
|
||||
{
|
||||
sourceBuilder.Append($" return {delegateName}(");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBuilder.Append($" {delegateName}(");
|
||||
}
|
||||
|
||||
sourceBuilder.Append(string.Join(", ", delegateInfo.Parameters.Select(p =>
|
||||
{
|
||||
string prefix = p.IsOutParameter ? "out " : p.IsRefParameter ? "ref " : string.Empty;
|
||||
return prefix + p.Name;
|
||||
})));
|
||||
|
||||
sourceBuilder.AppendLine(");");
|
||||
sourceBuilder.AppendLine(" }");
|
||||
}
|
||||
|
||||
// End class definition
|
||||
sourceBuilder.AppendLine(" }");
|
||||
sourceBuilder.AppendLine("}");
|
||||
|
||||
context.AddSource($"{classInfo.Name}.generated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
private static ClassInfo? GetClassInfoOrNull(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is not ClassDeclarationSyntax classDeclaration)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check attribute (support both NativeCallbacks and NativeCallbacksAttribute)
|
||||
bool hasNativeCallbacksAttribute = classDeclaration.AttributeLists
|
||||
.SelectMany(a => a.Attributes)
|
||||
.Any(a => a.Name.ToString() is "NativeCallbacks" or "NativeCallbacksAttribute");
|
||||
|
||||
if (!hasNativeCallbacksAttribute)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string namespaceName = classDeclaration.GetFullNamespace();
|
||||
|
||||
if (string.IsNullOrEmpty(namespaceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var classInfo = new ClassInfo
|
||||
{
|
||||
ClassDeclaration = classDeclaration,
|
||||
Name = classDeclaration.Identifier.ValueText,
|
||||
Namespace = namespaceName,
|
||||
Delegates = new List<DelegateInfo>(),
|
||||
NullableAwareable = context.SemanticModel.GetNullableContext(context.Node.Span.Start).HasFlag(NullableContext.AnnotationsEnabled)
|
||||
};
|
||||
|
||||
foreach (MemberDeclarationSyntax member in classDeclaration.Members)
|
||||
{
|
||||
if (member is not FieldDeclarationSyntax fieldDeclaration ||
|
||||
fieldDeclaration.Declaration.Type is not FunctionPointerTypeSyntax functionPointerTypeSyntax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var delegateInfo = new DelegateInfo
|
||||
{
|
||||
Name = fieldDeclaration.Declaration.Variables.First().Identifier.ValueText,
|
||||
Parameters = new List<DelegateParameterInfo>()
|
||||
};
|
||||
|
||||
char paramName = 'a';
|
||||
|
||||
for (int i = 0; i < functionPointerTypeSyntax.ParameterList.Parameters.Count; i++)
|
||||
{
|
||||
FunctionPointerParameterSyntax param = functionPointerTypeSyntax.ParameterList.Parameters[i];
|
||||
|
||||
DelegateParameterInfo parameter = new DelegateParameterInfo
|
||||
{
|
||||
Name = paramName.ToString(),
|
||||
IsOutParameter = param.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.OutKeyword)),
|
||||
IsRefParameter = param.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.RefKeyword)),
|
||||
Type = param.Type,
|
||||
};
|
||||
|
||||
bool isReturnParameter = i == functionPointerTypeSyntax.ParameterList.Parameters.Count - 1;
|
||||
if (isReturnParameter)
|
||||
{
|
||||
delegateInfo.ReturnValue = parameter;
|
||||
}
|
||||
else
|
||||
{
|
||||
delegateInfo.Parameters.Add(parameter);
|
||||
}
|
||||
|
||||
paramName++;
|
||||
}
|
||||
|
||||
classInfo.Delegates.Add(delegateInfo);
|
||||
}
|
||||
|
||||
return classInfo;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct ClassInfo
|
||||
{
|
||||
public ClassDeclarationSyntax ClassDeclaration;
|
||||
public string Name;
|
||||
public string Namespace;
|
||||
public List<DelegateInfo> Delegates;
|
||||
public bool NullableAwareable;
|
||||
}
|
||||
|
||||
internal struct DelegateInfo
|
||||
{
|
||||
public string Name;
|
||||
public List<DelegateParameterInfo> Parameters;
|
||||
public List<DelegateParameterInfo> ParametersAndReturnValue
|
||||
{
|
||||
get
|
||||
{
|
||||
List<DelegateParameterInfo> allParameters = new List<DelegateParameterInfo>(Parameters);
|
||||
|
||||
if (ReturnValue.Type.ToString() != "void")
|
||||
{
|
||||
allParameters.Add(ReturnValue);
|
||||
}
|
||||
|
||||
return allParameters;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasReturnValue => ReturnValue.Type.ToString() != "void";
|
||||
public DelegateParameterInfo ReturnValue;
|
||||
}
|
||||
|
||||
public struct DelegateParameterInfo
|
||||
{
|
||||
public string Name;
|
||||
public TypeSyntax Type;
|
||||
public bool IsOutParameter;
|
||||
public bool IsRefParameter;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"Generators": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "../UnrealSharp/UnrealSharp.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
internal static class RuleCategory
|
||||
{
|
||||
public const string Naming = nameof(Naming);
|
||||
public const string Category = nameof(Category);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators;
|
||||
|
||||
public class SourceBuilder : IDisposable
|
||||
{
|
||||
|
||||
private readonly StringBuilder _stringBuilder;
|
||||
private readonly IndentedTextWriter _indentedTextWriter;
|
||||
|
||||
public SourceBuilder()
|
||||
{
|
||||
_stringBuilder = new StringBuilder();
|
||||
_indentedTextWriter = new IndentedTextWriter(new StringWriter(_stringBuilder), " ");
|
||||
}
|
||||
|
||||
public int Indent
|
||||
{
|
||||
get => _indentedTextWriter.Indent;
|
||||
set => _indentedTextWriter.Indent = value;
|
||||
}
|
||||
|
||||
public Scope OpenBlock()
|
||||
{
|
||||
return new Scope(this);
|
||||
}
|
||||
|
||||
public SourceBuilder Append(string line)
|
||||
{
|
||||
_indentedTextWriter.Write(line);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder AppendLine(string line)
|
||||
{
|
||||
_indentedTextWriter.WriteLine(line);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder AppendLine()
|
||||
{
|
||||
return AppendLine(string.Empty);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_indentedTextWriter.Dispose();
|
||||
}
|
||||
|
||||
public readonly struct Scope : IDisposable
|
||||
{
|
||||
private readonly SourceBuilder _sourceBuilder;
|
||||
|
||||
internal Scope(SourceBuilder sourceBuilder)
|
||||
{
|
||||
_sourceBuilder = sourceBuilder;
|
||||
_sourceBuilder.AppendLine("{");
|
||||
_sourceBuilder.Indent++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_sourceBuilder.Indent--;
|
||||
_sourceBuilder.AppendLine("}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace UnrealSharp.SourceGenerators.StructGenerator;
|
||||
|
||||
[Generator]
|
||||
public class MarshalledStructGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider((n, _) => n is StructDeclarationSyntax or RecordDeclarationSyntax,
|
||||
(ctx, _) =>
|
||||
{
|
||||
var structDeclaration = ctx.Node;
|
||||
if (ctx.SemanticModel.GetDeclaredSymbol(structDeclaration) is not INamedTypeSymbol { TypeKind: TypeKind.Struct } structSymbol)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return AnalyzerStatics.HasAttribute(structSymbol, AnalyzerStatics.UStructAttribute)
|
||||
&& !structSymbol.Interfaces.Any(i => i.MetadataName == "MarshalledStruct`1")
|
||||
&& structSymbol.DeclaringSyntaxReferences
|
||||
.Select(r => r.GetSyntax())
|
||||
.OfType<TypeDeclarationSyntax>()
|
||||
.SelectMany(s => s.Modifiers)
|
||||
.Any(m => m.IsKind(SyntaxKind.PartialKeyword))
|
||||
? structSymbol
|
||||
: null;
|
||||
})
|
||||
.Where(sym => sym is not null);
|
||||
|
||||
context.RegisterSourceOutput(syntaxProvider, GenerateStruct!);
|
||||
}
|
||||
|
||||
private static void GenerateStruct(SourceProductionContext context, INamedTypeSymbol structSymbol)
|
||||
{
|
||||
using var builder = new SourceBuilder();
|
||||
|
||||
WriteStructCode(builder, structSymbol);
|
||||
|
||||
context.AddSource($"{structSymbol.Name}.generated.cs", builder.ToString());
|
||||
}
|
||||
|
||||
private static void WriteStructCode(SourceBuilder builder, INamedTypeSymbol structSymbol)
|
||||
{
|
||||
builder.AppendLine("using UnrealSharp;");
|
||||
builder.AppendLine("using UnrealSharp.Attributes;");
|
||||
builder.AppendLine("using UnrealSharp.Core.Marshallers;");
|
||||
builder.AppendLine("using UnrealSharp.Interop;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine($"namespace {structSymbol.ContainingNamespace.ToDisplayString()};");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine(
|
||||
$"partial {(structSymbol.IsRecord ? "record " : "")}struct {structSymbol.Name} : MarshalledStruct<{structSymbol.Name}>");
|
||||
|
||||
using var structScope = builder.OpenBlock();
|
||||
|
||||
builder.AppendLine("[WeaverGenerated]");
|
||||
builder.AppendLine("public static extern IntPtr GetNativeClassPtr();");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("[WeaverGenerated]");
|
||||
builder.AppendLine("public static extern int GetNativeDataSize();");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("[WeaverGenerated]");
|
||||
builder.AppendLine($"public static extern {structSymbol.Name} FromNative(IntPtr InNativeStruct);");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("[WeaverGenerated]");
|
||||
builder.AppendLine("public extern void ToNative(IntPtr buffer);");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<NoWarn>$(NoWarn);1570;0649;0169;0108;0109;RS1038</NoWarn>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,34 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace UnrealSharp.StaticVars;
|
||||
|
||||
public class FBaseStaticVar<T>
|
||||
{
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private T? _value;
|
||||
|
||||
public virtual T? Value
|
||||
{
|
||||
get => _value;
|
||||
set => _value = value;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
public FBaseStaticVar()
|
||||
{
|
||||
AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(GetType().Assembly)!;
|
||||
alc.Unloading += OnAlcUnloading;
|
||||
}
|
||||
|
||||
protected virtual void OnAlcUnloading(AssemblyLoadContext alc)
|
||||
{
|
||||
alc.Unloading -= OnAlcUnloading;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.StaticVars.Interop;
|
||||
|
||||
namespace UnrealSharp.StaticVars;
|
||||
|
||||
/// <summary>
|
||||
/// A static variable which will be alive during the whole game session.
|
||||
/// In editor the value will reset on Play In Editor start/end and on hot reload.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the static variable</typeparam>
|
||||
public sealed class FGameStaticVar<T> : FBaseStaticVar<T>
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly FDelegateHandle _onPieStartEndHandle;
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly FDelegateHandle _onPieEndHandle;
|
||||
|
||||
private readonly FEditorDelegates.FOnPIEEvent _onPIEndDelegate;
|
||||
|
||||
public FGameStaticVar()
|
||||
{
|
||||
_onPIEndDelegate = OnPIEStartEnd;
|
||||
IntPtr onPIEStartEndFuncPtr = Marshal.GetFunctionPointerForDelegate(_onPIEndDelegate);
|
||||
FEditorDelegatesExporter.CallBindEndPIE(onPIEStartEndFuncPtr, out _onPieEndHandle);
|
||||
FEditorDelegatesExporter.CallBindStartPIE(onPIEStartEndFuncPtr, out _onPieStartEndHandle);
|
||||
}
|
||||
|
||||
public FGameStaticVar(T value) : this()
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
~FGameStaticVar()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
protected override void OnAlcUnloading(AssemblyLoadContext alc)
|
||||
{
|
||||
base.OnAlcUnloading(alc);
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
private void OnPIEStartEnd(NativeBool simulating)
|
||||
{
|
||||
ResetToDefault();
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
ResetToDefault();
|
||||
FEditorDelegatesExporter.CallUnbindStartPIE(_onPieStartEndHandle);
|
||||
FEditorDelegatesExporter.CallUnbindEndPIE(_onPieEndHandle);
|
||||
}
|
||||
|
||||
void ResetToDefault()
|
||||
{
|
||||
Value = default;
|
||||
}
|
||||
|
||||
#else
|
||||
public FGameStaticVar(T value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
public static implicit operator T(FGameStaticVar<T> value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnrealSharp.Binds;
|
||||
using UnrealSharp.Core;
|
||||
|
||||
namespace UnrealSharp.StaticVars.Interop;
|
||||
|
||||
public struct FEditorDelegates
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void FOnPIEEvent(NativeBool sessionEnded);
|
||||
}
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FEditorDelegatesExporter
|
||||
{
|
||||
public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindStartPIE;
|
||||
public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindEndPIE;
|
||||
public static delegate* unmanaged<FDelegateHandle, void> UnbindStartPIE;
|
||||
public static delegate* unmanaged<FDelegateHandle, void> UnbindEndPIE;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using UnrealSharp.Binds;
|
||||
using UnrealSharp.Core;
|
||||
|
||||
namespace UnrealSharp.Interop;
|
||||
|
||||
public struct FWorldDelegates
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate void FWorldCleanupEvent(IntPtr world, NativeBool sessionEnded, NativeBool cleanupResources);
|
||||
}
|
||||
|
||||
[NativeCallbacks]
|
||||
public static unsafe partial class FWorldDelegatesExporter
|
||||
{
|
||||
public static delegate* unmanaged<IntPtr, out FDelegateHandle, void> BindOnWorldCleanup;
|
||||
public static delegate* unmanaged<FDelegateHandle, void> UnbindOnWorldCleanup;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DefineConstants>WITH_EDITOR</DefineConstants>
|
||||
<DefineConstants Condition="'$(DisableWithEditor)' == 'true'">$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))</DefineConstants>
|
||||
<DefineConstants Condition="'$(DefineAdditionalConstants)' != ''">$(DefineConstants);$(DefineAdditionalConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UnrealSharp.Core\UnrealSharp.Core.csproj" />
|
||||
<ProjectReference Include="..\UnrealSharp.SourceGenerators\UnrealSharp.SourceGenerators.csproj">
|
||||
<OutputItemType>Analyzer</OutputItemType>
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,73 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.Interop;
|
||||
|
||||
namespace UnrealSharp.StaticVars;
|
||||
|
||||
/// <summary>
|
||||
/// A static variable that has the lifetime of a UWorld. When the world is destroyed, the value is destroyed.
|
||||
/// For example when traveling between levels, the value is destroyed.
|
||||
/// </summary>
|
||||
public sealed class FWorldStaticVar<T> : FBaseStaticVar<T>
|
||||
{
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly Dictionary<IntPtr, T> _worldToValue = new Dictionary<IntPtr, T>();
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private readonly FDelegateHandle _onWorldCleanupHandle;
|
||||
|
||||
public FWorldStaticVar()
|
||||
{
|
||||
FWorldDelegates.FWorldCleanupEvent onWorldCleanupDelegate = OnWorldCleanup;
|
||||
IntPtr onWorldCleanup = Marshal.GetFunctionPointerForDelegate(onWorldCleanupDelegate);
|
||||
FWorldDelegatesExporter.CallBindOnWorldCleanup(onWorldCleanup, out _onWorldCleanupHandle);
|
||||
}
|
||||
|
||||
public FWorldStaticVar(T value) : this()
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
~FWorldStaticVar()
|
||||
{
|
||||
FWorldDelegatesExporter.CallUnbindOnWorldCleanup(_onWorldCleanupHandle);
|
||||
}
|
||||
|
||||
public override T? Value
|
||||
{
|
||||
get => GetWorldValue();
|
||||
set => SetWorldValue(value!);
|
||||
}
|
||||
|
||||
private T? GetWorldValue()
|
||||
{
|
||||
IntPtr worldPtr = FCSManagerExporter.CallGetCurrentWorldPtr();
|
||||
return _worldToValue.GetValueOrDefault(worldPtr);
|
||||
}
|
||||
|
||||
private void SetWorldValue(T value)
|
||||
{
|
||||
IntPtr worldPtr = FCSManagerExporter.CallGetCurrentWorldPtr();
|
||||
if (_worldToValue.TryAdd(worldPtr, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_worldToValue[worldPtr] = value;
|
||||
}
|
||||
|
||||
private void OnWorldCleanup(IntPtr world, NativeBool sessionEnded, NativeBool cleanupResources)
|
||||
{
|
||||
_worldToValue.Remove(world);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
protected override void OnAlcUnloading(AssemblyLoadContext alc)
|
||||
{
|
||||
base.OnAlcUnloading(alc);
|
||||
FWorldDelegatesExporter.CallUnbindOnWorldCleanup(_onWorldCleanupHandle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
202
Plugins/UnrealSharp/Managed/UnrealSharp/UnrealSharp/Array.cs
Normal file
202
Plugins/UnrealSharp/Managed/UnrealSharp/UnrealSharp/Array.cs
Normal file
@ -0,0 +1,202 @@
|
||||
using UnrealSharp.Attributes;
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.Core.Attributes;
|
||||
using UnrealSharp.Core.Marshallers;
|
||||
using UnrealSharp.Interop;
|
||||
|
||||
namespace UnrealSharp;
|
||||
|
||||
/// <summary>
|
||||
/// An array that can be used to interact with Unreal Engine arrays.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"> The type of elements in the array. </typeparam>
|
||||
[Binding]
|
||||
public class TArray<T> : UnrealArrayBase<T>, IList<T>, IReadOnlyList<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public TArray(IntPtr nativeUnrealProperty, IntPtr nativeBuffer, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative)
|
||||
: base(nativeUnrealProperty, nativeBuffer, toNative, fromNative)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
{
|
||||
throw new IndexOutOfRangeException($"Index {index} is out of bounds. Array size is {Count}.");
|
||||
}
|
||||
return Get(index);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
{
|
||||
throw new IndexOutOfRangeException($"Index {index} is out of bounds. Array size is {Count}.");
|
||||
}
|
||||
ToNative(NativeArrayBuffer, index, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the end of the array.
|
||||
/// </summary>
|
||||
/// <param name="item"> The element to add. </param>
|
||||
public void Add(T item)
|
||||
{
|
||||
int newIndex = Count;
|
||||
AddInternal();
|
||||
this[newIndex] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all elements from the array.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ClearInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes the array to the specified size.
|
||||
/// If the new size is smaller than the current size, elements will be removed. If the new size is larger, elements will be added.
|
||||
/// </summary>
|
||||
/// <param name="newSize"> The new size of the array. </param>
|
||||
public void Resize(int newSize)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
FArrayPropertyExporter.CallResizeArray(NativeProperty, NativeBuffer, newSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the elements at the specified indices.
|
||||
/// </summary>
|
||||
/// <param name="indexA"> The index of the first element to swap. </param>
|
||||
/// <param name="indexB"> The index of the second element to swap. </param>
|
||||
public void Swap(int indexA, int indexB)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
FArrayPropertyExporter.CallSwapValues(NativeProperty, NativeBuffer, indexA, indexB);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the elements of the array to an array starting at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="array"> The array to copy the elements to. </param>
|
||||
/// <param name="arrayIndex"> The index in the array to start copying to. </param>
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
int numElements = Count;
|
||||
for (int i = 0; i < numElements; ++i)
|
||||
{
|
||||
array[i + arrayIndex] = this[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the first occurrence of a specific object from the array.
|
||||
/// </summary>
|
||||
/// <param name="item"> The object to remove. </param>
|
||||
/// <returns> True if the object was successfully removed; otherwise, false. This method also returns false if the object is not found in the array. </returns>
|
||||
public bool Remove(T item)
|
||||
{
|
||||
int index = IndexOf(item);
|
||||
if (index != -1)
|
||||
{
|
||||
RemoveAt(index);
|
||||
}
|
||||
return index != -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the specified element in the array.
|
||||
/// </summary>
|
||||
/// <param name="item"> The element to find. </param>
|
||||
/// <returns> The index of the element in the array, or -1 if the element is not in the array. </returns>
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
int numElements = Count;
|
||||
for (int i = 0; i < numElements; ++i)
|
||||
{
|
||||
if (this[i].Equals(item))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts an element into the array at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index"> The index to insert the element at. </param>
|
||||
/// <param name="item"> The element to insert. </param>
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
InsertInternal(index);
|
||||
this[index] = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the element at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index"> The index of the element to remove. </param>
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
RemoveAtInternal(index);
|
||||
}
|
||||
}
|
||||
|
||||
public class ArrayMarshaller<T>(IntPtr nativeProperty, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative)
|
||||
{
|
||||
private TArray<T>? _arrayWrapper;
|
||||
|
||||
public void ToNative(IntPtr nativeBuffer, int arrayIndex, IList<T> obj)
|
||||
{
|
||||
ToNative(nativeBuffer, obj);
|
||||
}
|
||||
|
||||
public void ToNative(IntPtr nativeBuffer, IList<T> obj)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UnmanagedArray* mirror = (UnmanagedArray*)nativeBuffer;
|
||||
if (mirror->ArrayNum == obj.Count)
|
||||
{
|
||||
for (int i = 0; i < obj.Count; ++i)
|
||||
{
|
||||
toNative(mirror->Data, i, obj[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FArrayPropertyExporter.CallResizeArray(nativeProperty, mirror, obj.Count);
|
||||
for (int i = 0; i < obj.Count; ++i)
|
||||
{
|
||||
toNative(mirror->Data, i, obj[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TArray<T> FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
if (_arrayWrapper == null)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_arrayWrapper = new TArray<T>(nativeProperty, nativeBuffer + arrayIndex * sizeof(UnmanagedArray), toNative, fromNative);
|
||||
}
|
||||
}
|
||||
return _arrayWrapper;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
using UnrealSharp.Core;
|
||||
using UnrealSharp.Core.Marshallers;
|
||||
using UnrealSharp.Interop;
|
||||
|
||||
namespace UnrealSharp;
|
||||
|
||||
public class TArrayReadOnly<T> : UnrealArrayBase<T>, IReadOnlyList<T>
|
||||
{
|
||||
public TArrayReadOnly(IntPtr nativeUnrealProperty, IntPtr nativeBuffer, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative)
|
||||
: base(nativeUnrealProperty, nativeBuffer, toNative, fromNative)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T this[int index] => Get(index);
|
||||
}
|
||||
|
||||
public class ArrayReadOnlyMarshaller<T>(IntPtr nativeProperty, MarshallingDelegates<T>.ToNative toNative, MarshallingDelegates<T>.FromNative fromNative)
|
||||
{
|
||||
private TArrayReadOnly<T>? _readOnlyWrapper;
|
||||
|
||||
public void ToNative(IntPtr nativeBuffer, IReadOnlyList<T> obj)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
UnmanagedArray* mirror = (UnmanagedArray*)nativeBuffer;
|
||||
if (mirror->ArrayNum == obj.Count)
|
||||
{
|
||||
for (int i = 0; i < obj.Count; ++i)
|
||||
{
|
||||
toNative(mirror->Data, i, obj[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FArrayPropertyExporter.CallResizeArray(nativeProperty, mirror, obj.Count);
|
||||
for (int i = 0; i < obj.Count; ++i)
|
||||
{
|
||||
toNative(mirror->Data, i, obj[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TArrayReadOnly<T> FromNative(IntPtr nativeBuffer, int arrayIndex)
|
||||
{
|
||||
if (_readOnlyWrapper == null)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
_readOnlyWrapper = new TArrayReadOnly<T>(nativeProperty, nativeBuffer + arrayIndex * sizeof(UnmanagedArray), toNative, fromNative);
|
||||
}
|
||||
}
|
||||
return _readOnlyWrapper;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace UnrealSharp.Attributes;
|
||||
|
||||
public class BaseUAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The display name of the type, used in the editor.
|
||||
/// </summary>
|
||||
public string DisplayName = "";
|
||||
|
||||
/// <summary>
|
||||
/// The category of the type, used in the editor.
|
||||
/// </summary>
|
||||
public string Category = "";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user