From 78441b5e69b28119fa7d363e1111f3fa1995ae48 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 12:19:35 +0100 Subject: [PATCH 01/14] InternalCallGenerator first version --- .../InternalCallMethod.cs | 15 + .../InternalCallSourceGenerator.cs | 110 ++++++ .../Properties/launchSettings.json | 8 + ...pPluginLoader.InternalCallGenerator.csproj | 26 ++ .../SourceGenerationHelper.cs | 365 ++++++++++++++++++ mhw-cs-plugin-loader.sln | 27 ++ 6 files changed, 551 insertions(+) create mode 100644 SharpPluginLoader.InternalCallGenerator/InternalCallMethod.cs create mode 100644 SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs create mode 100644 SharpPluginLoader.InternalCallGenerator/Properties/launchSettings.json create mode 100644 SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj create mode 100644 SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs diff --git a/SharpPluginLoader.InternalCallGenerator/InternalCallMethod.cs b/SharpPluginLoader.InternalCallGenerator/InternalCallMethod.cs new file mode 100644 index 0000000..0cc0aa1 --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/InternalCallMethod.cs @@ -0,0 +1,15 @@ +using Microsoft.CodeAnalysis; + +namespace SharpPluginLoader.InternalCallGenerator; + +public struct InternalCallMethod(IMethodSymbol method, bool isUnsafe) +{ + public IMethodSymbol Method = method; + public bool IsUnsafe = isUnsafe; + + public readonly void Deconstruct(out IMethodSymbol outMethod, out bool outIsUnsafe) + { + outMethod = Method; + outIsUnsafe = IsUnsafe; + } +} diff --git a/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs b/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs new file mode 100644 index 0000000..1321a7d --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace SharpPluginLoader.InternalCallGenerator; + +[Generator] +public class InternalCallSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "InternalCallAttribute.g.cs", + SourceText.From(SourceGenerationHelper.Attribute, Encoding.UTF8) + )); + + var internalCalls = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsSyntaxTargetForGeneration(s), + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)!) + .Where(static m => m is not null); + + IncrementalValueProvider<(Compilation, ImmutableArray)> compilationAndInternalCalls + = context.CompilationProvider.Combine(internalCalls.Collect()); + + context.RegisterSourceOutput(compilationAndInternalCalls, + static (spc, source) => Execute(source.Item1, source.Item2, spc)); + } + + public static void Execute(Compilation compilation, ImmutableArray methods, + SourceProductionContext context) + { + if (methods.IsDefaultOrEmpty) + return; + + var distinctICalls = methods.Distinct(); + + var methodsToGenerate = GetMethodsToGenerate(compilation, distinctICalls, context.CancellationToken); + + if (methodsToGenerate.Count == 0) + return; + + var source = SourceGenerationHelper.GenerateSource(methodsToGenerate, context); + context.AddSource("InternalCalls.g.cs", SourceText.From(source, Encoding.UTF8)); + } + + public static List GetMethodsToGenerate(Compilation compilation, + IEnumerable methods, CancellationToken ct) + { + List methodsToGenerate = []; + + var icallAttribute = compilation.GetTypeByMetadataName(SourceGenerationHelper.FullAttributeName); + if (icallAttribute is null) + return methodsToGenerate; + + foreach (var method in methods) + { + ct.ThrowIfCancellationRequested(); + + var semanticModel = compilation.GetSemanticModel(method.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(method) is not IMethodSymbol methodSymbol) + continue; + + if (methodSymbol.IsGenericMethod || !methodSymbol.IsPartialDefinition) // Generic methods are not supported + continue; + + var attributeData = methodSymbol.GetAttributes().FirstOrDefault( + ad => SymbolEqualityComparer.Default.Equals(ad.AttributeClass, icallAttribute)); + var isUnsafe = attributeData?.ConstructorArguments + .Any(tc => tc.Type?.ToDisplayString() == SourceGenerationHelper.FullOptionsEnumName + && tc.Value is not null + && (int)tc.Value == 1) ?? false; + methodsToGenerate.Add(new InternalCallMethod(methodSymbol, isUnsafe)); + } + + return methodsToGenerate; + } + + public static bool IsSyntaxTargetForGeneration(SyntaxNode syntax) + { + return syntax is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + } + + public static MethodDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) + { + var method = (MethodDeclarationSyntax)context.Node; + + foreach (var attributeList in method.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol) + continue; + + var attributeType = attributeSymbol.ContainingType; + var fullName = attributeType?.ToDisplayString() ?? string.Empty; + + if (fullName == SourceGenerationHelper.FullAttributeName) + return method; + } + } + + return null; + } +} diff --git a/SharpPluginLoader.InternalCallGenerator/Properties/launchSettings.json b/SharpPluginLoader.InternalCallGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..58b7ae2 --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Examples\\ExperimentalTesting\\ExperimentalTesting.csproj" + } + } +} \ No newline at end of file diff --git a/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj b/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj new file mode 100644 index 0000000..27d5bfe --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + 12.0 + True + InternalCall Generator + false + enable + true + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs new file mode 100644 index 0000000..c7bdc83 --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace SharpPluginLoader.InternalCallGenerator; + +using TransformedType = (string? typeName, TypeConversionKind conversion); + +public static class SourceGenerationHelper +{ + public const string GeneratorNamespace = "SharpPluginLoader.InternalCallGenerator"; + public const string AttributeName = "InternalCallAttribute"; + public const string OptionsEnumName = "InternalCallOptions"; + public const string FullAttributeName = $"{GeneratorNamespace}.{AttributeName}"; + public const string FullOptionsEnumName = $"{GeneratorNamespace}.{OptionsEnumName}"; + public const string Attribute = $$""" + namespace {{GeneratorNamespace}}; + + public enum InternalCallOptions + { + None = 0, + + // Suppresses the GC transition when calling the function pointer + Unsafe = 1 + } + + [System.AttributeUsage(System.AttributeTargets.Method)] + public class {{AttributeName}}(InternalCallOptions options = InternalCallOptions.None) : System.Attribute + { + public InternalCallOptions Options { get; } = options; + } + """; + + public static string GenerateSource(List internalCalls, SourceProductionContext context) + { + if (internalCalls.Count == 0) + return ""; + + var sb = new StringBuilder(); + var fieldTypeSb = new StringBuilder(); // Used to generate the fields + var methodSb = new StringBuilder(); // Used to generate the methods + var methodBodySb = new StringBuilder(); // Used to generate the method body, cleared after each method + var uploadMethodSb = new StringBuilder(); // Used to generate the upload method + List methodInvokeParams = []; // Used to store variable names for the function pointer invocation + List pinStatements = []; // Used to store variables that need to be pinned + List gcHandles = []; // Used to store the gc handles + + sb.AppendLine("using System;"); + sb.AppendLine("using System.Runtime.InteropServices;"); + sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using SharpPluginLoader.Core;"); + sb.AppendLine("using SharpPluginLoader.Core.Memory;"); + sb.Append($$""" + + namespace {{internalCalls[0].Method.ContainingNamespace.ToDisplayString()}}; + + public static unsafe partial class InternalCalls + { + + """); + + // Generate start of upload method + uploadMethodSb.Append(""" + + public static void UploadInternalCalls(System.Collections.Generic.Dictionary icalls) + { + + """); + + // Each internal call needs + // - A field of the form: private static delegate* unmanaged Ptr; + // - A method of the form: public static unsafe partial R (T...); + + // The allowed parameter/return types are: + // - Primitive types + // - Pointer types + // - Array types where the element type is a primitive type or a pointer type + // - ref/out types + // - string + foreach (var (method, isUnsafe) in internalCalls) + { + var transformedReturnType = TransformReturn(method, context); + if (transformedReturnType.typeName is null) + continue; + + // Start field generation + fieldTypeSb.Append(isUnsafe + ? "delegate* unmanaged[SuppressGCTransition]<" + : "delegate* unmanaged<"); + + var retModifier = ""; + if (method.ReturnsByRef) + retModifier = "ref "; + else if (method.ReturnsByRefReadonly) + retModifier = "ref readonly "; + + // Start method generation + methodSb.Append($"public static partial {retModifier}{method.ReturnType.ToDisplayString()} {method.Name}("); + + if (!method.ReturnsVoid) + methodBodySb.AppendLine($"{transformedReturnType.typeName} __result = default;"); + + for (var i = 0; i < method.Parameters.Length; ++i) + { + var param = method.Parameters[i]; + var (typeName, conversion) = TransformParameter(param, context); + + if (typeName is null) + continue; + + var isLast = i == method.Parameters.Length - 1; + + // Add the type to the field + fieldTypeSb.Append(typeName); + fieldTypeSb.Append(", "); // Comma always added as the return type is last + + // Add the type to the method + methodSb.Append(param.ToDisplayString()); + if (!isLast) + methodSb.Append(", "); + + string invokeParamName; + switch (conversion) + { + case TypeConversionKind.Array: + // Need to pin the array and get a pointer to the first element + invokeParamName = $"t__{param.Name}"; + methodBodySb.AppendLine($"GCHandle gch_{param.Name} = GCHandle.Alloc({param.Name}, GCHandleType.Pinned);"); + methodBodySb.AppendLine( + $"{typeName} {invokeParamName} = MemoryUtil.AsPointer(ref MemoryMarshal.GetArrayDataReference({param.Name}));"); + methodInvokeParams.Add(invokeParamName); + gcHandles.Add($"gch_{param.Name}.Free();"); + break; + case TypeConversionKind.RefOut: + switch (param.RefKind) + { + case RefKind.Ref: + invokeParamName = $"t__{param.Name}"; + pinStatements.Add($"fixed({typeName} {invokeParamName} = &{param.Name})"); + methodInvokeParams.Add(invokeParamName); + break; + case RefKind.Out: + methodBodySb.AppendLine($"Unsafe.SkipInit(out {param.Name});"); + invokeParamName = $"t__{param.Name}"; + pinStatements.Add($"fixed({typeName} {invokeParamName} = &{param.Name})"); + methodInvokeParams.Add(invokeParamName); + break; + case RefKind.In: + ReportError(context, "ICG003", "In parameters are not supported"); + methodInvokeParams.Add(param.Name); + break; + case RefKind.RefReadOnlyParameter: + ReportError(context, "ICG004", "Ref readonly parameters are not supported"); + methodInvokeParams.Add(param.Name); + break; + case RefKind.None: + default: + throw new ArgumentOutOfRangeException(); + } + break; + case TypeConversionKind.String: + throw new NotImplementedException(); + case TypeConversionKind.None: + default: + methodInvokeParams.Add(param.Name); + break; + } + } + + // Write the pin statements + foreach (var pinStatement in pinStatements) + methodBodySb.AppendLine(pinStatement); + + // Invoke the function pointer + methodBodySb.AppendLine(transformedReturnType.typeName == "void" + ? $"_{method.Name}Ptr({string.Join(", ", methodInvokeParams)});" + : $"__result = _{method.Name}Ptr({string.Join(", ", methodInvokeParams)});"); + + foreach (var gch in gcHandles) + methodBodySb.AppendLine(gch); + + // Add the return statement + if (transformedReturnType.typeName != "void") + { + switch (transformedReturnType.conversion) + { + case TypeConversionKind.None: + methodBodySb.AppendLine("return __result;"); + break; + case TypeConversionKind.Array: + throw new InvalidOperationException("Array return types are not supported"); + case TypeConversionKind.RefOut: + methodBodySb.AppendLine("return ref *__result;"); + break; + case TypeConversionKind.String: + throw new NotImplementedException(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + // End field type generation + fieldTypeSb.Append($"{transformedReturnType.typeName}>"); + + // Generate field + sb.AppendLine($"private static {fieldTypeSb} _{method.Name}Ptr;"); + + // End method generation + methodSb.AppendLine(")"); + methodSb.Append($$""" + { + {{methodBodySb}} + } + """); + + // Generate method + sb.AppendLine(methodSb.ToString()); + + // Add entry to upload method + var fieldType = fieldTypeSb.ToString(); + uploadMethodSb.AppendLine($"_{method.Name}Ptr = ({fieldType})icalls[\"{method.Name}\"];"); + + // Clear reused string builders and lists + methodBodySb.Clear(); + methodInvokeParams.Clear(); + pinStatements.Clear(); + gcHandles.Clear(); + fieldTypeSb.Clear(); + methodSb.Clear(); + } + + // End upload method + uploadMethodSb.AppendLine("}"); + + // Generate upload method + sb.AppendLine(uploadMethodSb.ToString()); + + // End class + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static TransformedType TransformParameter(IParameterSymbol param, SourceProductionContext context) + { + switch (param.Type) + { + case IArrayTypeSymbol arrayType: + if (param.RefKind != RefKind.None) + { + ReportError(context, "ICG005", "Ref/out array parameters are not supported"); + return (null, TypeConversionKind.None); + } + return (TransformType(arrayType.ElementType, context).typeName + "*", TypeConversionKind.Array); + case IPointerTypeSymbol pointerType: + var (typeName, _) = TransformType(pointerType.PointedAtType, context); + return param.RefKind is RefKind.Ref or RefKind.Out + ? (typeName + "**", TypeConversionKind.RefOut) // ref/out pointers are represented as pointer to pointer + : (typeName + "*", TypeConversionKind.None); + case INamedTypeSymbol namedType: + if (namedType.IsGenericType || !namedType.IsValueType) + { + ReportError(context, "ICG001", "Generic types and reference types are not supported"); + return (null, TypeConversionKind.None); + } + + var (type, _) = TransformType(namedType, context); + return param.RefKind is RefKind.Ref or RefKind.Out + ? (type + "*", TypeConversionKind.RefOut) // ref/out value types are represented as pointer to value type + : (type, TypeConversionKind.None); + case ITypeParameterSymbol: + ReportError(context, "ICG002", "Type parameters are not supported"); + return (null, TypeConversionKind.None); + default: + return (param.Type.ToDisplayString(), TypeConversionKind.None); + } + } + + private static TransformedType TransformReturn(IMethodSymbol method, SourceProductionContext context) + { + if (method.ReturnsVoid) + return ("void", TypeConversionKind.None); + + switch (method.ReturnType) + { + case IArrayTypeSymbol: + ReportError(context, "ICG006", "Array return types are not supported. Use a pointer instead."); + return (null, TypeConversionKind.None); + case IPointerTypeSymbol pointerType: + var (typeName, _) = TransformType(pointerType.PointedAtType, context); + return method.ReturnsByRef + ? (typeName + "**", TypeConversionKind.RefOut) + : (typeName + "*", TypeConversionKind.None); + case INamedTypeSymbol namedType: + if (namedType.IsGenericType || !namedType.IsValueType) + { + ReportError(context, "ICG001", "Generic types and reference types are not supported"); + return (null, TypeConversionKind.None); + } + + var (type, _) = TransformType(namedType, context); + return method.ReturnsByRef + ? (type + "*", TypeConversionKind.RefOut) + : (type, TypeConversionKind.None); + case ITypeParameterSymbol: + ReportError(context, "ICG002", "Type parameters are not supported"); + return (null, TypeConversionKind.None); + default: + return (method.ReturnType.ToDisplayString(), TypeConversionKind.None); + } + } + + private static TransformedType TransformType(ITypeSymbol type, SourceProductionContext context) + { + switch (type) + { + case IArrayTypeSymbol arrayType: + return (TransformType(arrayType.ElementType, context).typeName + "*", TypeConversionKind.Array); + case IPointerTypeSymbol pointerType: + return (TransformType(pointerType.PointedAtType, context).typeName + "*", TypeConversionKind.None); + case INamedTypeSymbol namedType: + if (namedType.IsGenericType || !namedType.IsValueType) + { + ReportError(context, "ICG001", "Generic types and reference types are not supported"); + return (null, TypeConversionKind.None); + } + + return namedType.IsRefLikeType + ? ($"{namedType.ToDisplayString()}*", TypeConversionKind.RefOut) + : (namedType.ToDisplayString(), TypeConversionKind.None); + case ITypeParameterSymbol: + ReportError(context, "ICG002", "Type parameters are not supported"); + return (null, TypeConversionKind.None); + default: + return (type.ToDisplayString(), TypeConversionKind.None); + } + } + + private static void ReportError(SourceProductionContext context, string id, string message) + { + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + id, + "InternalCallGenerator", + message, + "InternalCallGenerator", + DiagnosticSeverity.Error, + true + ), + null + ) + ); + } +} + +internal enum TypeConversionKind +{ + None = 0, + Array = 1, + RefOut = 2, + String = 3 +} diff --git a/mhw-cs-plugin-loader.sln b/mhw-cs-plugin-loader.sln index 2f0ea76..a3f3313 100644 --- a/mhw-cs-plugin-loader.sln +++ b/mhw-cs-plugin-loader.sln @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomPackets", "Examples\C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PreloadTesting", "Examples\PreloadTesting\PreloadTesting.csproj", "{C37E5763-6EBE-45CE-A903-DEF1AEA64FEA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpPluginLoader.InternalCallGenerator", "SharpPluginLoader.InternalCallGenerator\SharpPluginLoader.InternalCallGenerator.csproj", "{CBD13575-CA0C-4EB5-821D-847A6EE5581F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -313,6 +315,30 @@ Global {C37E5763-6EBE-45CE-A903-DEF1AEA64FEA}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {C37E5763-6EBE-45CE-A903-DEF1AEA64FEA}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {C37E5763-6EBE-45CE-A903-DEF1AEA64FEA}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|x64.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|x64.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Debug|x86.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|x86.ActiveCfg = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.MinSizeRel|x86.Build.0 = Debug|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|Any CPU.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|x64.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|x64.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|x86.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.Release|x86.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -329,6 +355,7 @@ Global {5EA27F04-B731-4766-88A4-221E659D9570} = {93226706-71AC-41BC-A457-21D3CAA8C751} {444FF57A-C89D-403C-ABFB-2CC6BA1D9FCE} = {93226706-71AC-41BC-A457-21D3CAA8C751} {C37E5763-6EBE-45CE-A903-DEF1AEA64FEA} = {93226706-71AC-41BC-A457-21D3CAA8C751} + {CBD13575-CA0C-4EB5-821D-847A6EE5581F} = {22614599-FA47-4E69-8DBB-A183F0A4AE25} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {623F8CBC-2C3B-488D-B7BB-0140A8201BB7} From 034a3832defd1d18bc29abfd95d153eaaa9dc66b Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 12:20:13 +0100 Subject: [PATCH 02/14] InternalCallManager attribute --- SharpPluginLoader.Core/InternalCallManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SharpPluginLoader.Core/InternalCallManager.cs b/SharpPluginLoader.Core/InternalCallManager.cs index 47160cc..986da0d 100644 --- a/SharpPluginLoader.Core/InternalCallManager.cs +++ b/SharpPluginLoader.Core/InternalCallManager.cs @@ -11,6 +11,15 @@ internal readonly struct InternalCall public string Name => Marshal.PtrToStringAnsi(_fieldName) ?? string.Empty; } + /// + /// This attribute is used to mark a class as an internal call manager. + /// There can only be one internal call manager per plugin. + /// + /// The internal call manager is where the plugin keeps all of its internal calls. + /// + [AttributeUsage(AttributeTargets.Class)] + public class InternalCallManagerAttribute : Attribute; + internal static class InternalCallManager { public static unsafe void UploadInternalCalls(InternalCall* internalCalls, uint internalCallsCount) From 9b2ffd56214f7067c9e5c5a20ac9d05a2b6b9dcb Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 12:20:30 +0100 Subject: [PATCH 03/14] InternalCall tests --- .../ExperimentalTesting.csproj | 5 ++ Examples/ExperimentalTesting/InternalCalls.cs | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Examples/ExperimentalTesting/InternalCalls.cs diff --git a/Examples/ExperimentalTesting/ExperimentalTesting.csproj b/Examples/ExperimentalTesting/ExperimentalTesting.csproj index db1fd4c..dcfb1d6 100644 --- a/Examples/ExperimentalTesting/ExperimentalTesting.csproj +++ b/Examples/ExperimentalTesting/ExperimentalTesting.csproj @@ -4,6 +4,8 @@ net8.0 enable enable + True + true @@ -11,6 +13,9 @@ False False + diff --git a/Examples/ExperimentalTesting/InternalCalls.cs b/Examples/ExperimentalTesting/InternalCalls.cs new file mode 100644 index 0000000..e48d1a8 --- /dev/null +++ b/Examples/ExperimentalTesting/InternalCalls.cs @@ -0,0 +1,49 @@ +using SharpPluginLoader.InternalCallGenerator; +using SharpPluginLoader.Core.MtTypes; +using SharpPluginLoader.Core; + +namespace ExperimentalTesting; + +[InternalCallManager] +public unsafe partial class InternalCalls +{ + // Test with simple primitive types + [InternalCall] + public static partial long Sum(long a, long b); + + // Test with single pointer type + [InternalCall] + public static partial void TestPointers(double* values); + + // Test with mixed unmanaged types + [InternalCall] + public static partial char ProcessData(byte* data, int length, ref ushort dataId); + + // Test with array and pointer types + [InternalCall] + public static partial void TransformCoordinates(float[] coords, MtVector3* transformMatrix); + + // Test with InternalCallOptions and ref parameter + [InternalCall(InternalCallOptions.Unsafe)] + public static partial void ModifyValue(ref int value); + + // Test with no return value and primitive parameters + [InternalCall] + public static partial void LogMessage(int level, char* message); + + // Test with complex struct return type and primitive parameters + [InternalCall] + public static partial MtMatrix4X4 ComputeStruct(int param1, float param2); + + // Test with pointer to struct and primitive types + [InternalCall] + public static partial MtVector2* CreateVector(int x, int y); + + // Test with multiple ref and out parameters + [InternalCall] + public static partial void TestRefOutParameters(ref float a, out double b, ref MtVector2 vec); + + // Test with mixed array, pointer, and primitive types + [InternalCall] + public static partial void ProcessBuffers(byte[] inputBuffer, float* outputBuffer, int bufferSize); +} From 4b2a4b74b4b62cfdddf440365855e4bc87415665 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 13:00:49 +0100 Subject: [PATCH 04/14] Follow safe vs unsafe method declarations --- .../SourceGenerationHelper.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs index c7bdc83..719640c 100644 --- a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs +++ b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace SharpPluginLoader.InternalCallGenerator; @@ -96,8 +99,10 @@ public static void UploadInternalCalls(System.Collections.Generic.Dictionary Date: Sat, 6 Jan 2024 18:28:04 +0100 Subject: [PATCH 05/14] MemoryUtil string functionality --- SharpPluginLoader.Core/Memory/MemoryUtil.cs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/SharpPluginLoader.Core/Memory/MemoryUtil.cs b/SharpPluginLoader.Core/Memory/MemoryUtil.cs index 40153dc..5d4d0e8 100644 --- a/SharpPluginLoader.Core/Memory/MemoryUtil.cs +++ b/SharpPluginLoader.Core/Memory/MemoryUtil.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; namespace SharpPluginLoader.Core.Memory { @@ -253,6 +254,64 @@ public static void Free(void* address) #endregion + #region Strings + + /// + /// Gets the length of a null-terminated string at a given address. + /// + /// The address of the string + /// The length of the string + /// + /// This method counts the number of bytes, not the number of characters. + /// + public static int StringLength(nint address) + { + var ptr = (byte*)address; + var length = 0; + + while (*ptr != 0) + { + ptr++; + length++; + } + + return length; + } + + /// + /// Reads a null-terminated string from a given address. + /// + /// The address of the string + /// The encoding to use, or UTF8 if no encoding is given + /// The string read + public static string ReadString(nint address, Encoding? encoding = null) + { + var length = StringLength(address); + return (encoding ?? Encoding.UTF8).GetString((byte*)address, length); + } + + #region Mirror Methods for long + + /// + public static int StringLength(long address) => StringLength((nint)address); + + /// + public static string ReadString(long address, Encoding? encoding = null) => ReadString((nint)address, encoding); + + #endregion + + #region Mirror Methods for byte* + + /// + public static int StringLength(byte* address) => StringLength((nint)address); + + /// + public static string ReadString(byte* address, Encoding? encoding = null) => ReadString((nint)address, encoding); + + #endregion + + #endregion + /// /// Creates a span around a native array from a given address and count. /// From 3e6fd0076c4a997c069c2c49dee90d5947f4d15b Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 19:26:18 +0100 Subject: [PATCH 06/14] String support for internal calls --- .../InternalCallSourceGenerator.cs | 2 +- .../SourceGenerationHelper.cs | 79 +++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs b/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs index 1321a7d..04183e6 100644 --- a/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs +++ b/SharpPluginLoader.InternalCallGenerator/InternalCallSourceGenerator.cs @@ -73,7 +73,7 @@ public static List GetMethodsToGenerate(Compilation compilat ad => SymbolEqualityComparer.Default.Equals(ad.AttributeClass, icallAttribute)); var isUnsafe = attributeData?.ConstructorArguments .Any(tc => tc.Type?.ToDisplayString() == SourceGenerationHelper.FullOptionsEnumName - && tc.Value is not null + && tc.Value is not null && (int)tc.Value == 1) ?? false; methodsToGenerate.Add(new InternalCallMethod(methodSymbol, isUnsafe)); } diff --git a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs index 719640c..392803b 100644 --- a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs +++ b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -15,8 +16,10 @@ public static class SourceGenerationHelper public const string GeneratorNamespace = "SharpPluginLoader.InternalCallGenerator"; public const string AttributeName = "InternalCallAttribute"; public const string OptionsEnumName = "InternalCallOptions"; + public const string WideStringAttributeName = "WideStringAttribute"; public const string FullAttributeName = $"{GeneratorNamespace}.{AttributeName}"; public const string FullOptionsEnumName = $"{GeneratorNamespace}.{OptionsEnumName}"; + public const string FullWideStringAttributeName = $"{GeneratorNamespace}.{WideStringAttributeName}"; public const string Attribute = $$""" namespace {{GeneratorNamespace}}; @@ -33,8 +36,13 @@ public class {{AttributeName}}(InternalCallOptions options = InternalCallOptions { public InternalCallOptions Options { get; } = options; } + + [System.AttributeUsage(System.AttributeTargets.Parameter | System.AttributeTargets.ReturnValue)] + public class {{WideStringAttributeName}} : System.Attribute; """; + private static IMethodSymbol _currentMethodSymbol; + public static string GenerateSource(List internalCalls, SourceProductionContext context) { if (internalCalls.Count == 0) @@ -50,6 +58,7 @@ public static string GenerateSource(List internalCalls, Sour List gcHandles = []; // Used to store the gc handles sb.AppendLine("using System;"); + sb.AppendLine("using System.Text;"); sb.AppendLine("using System.Runtime.InteropServices;"); sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using System.Collections.Generic;"); @@ -84,6 +93,8 @@ public static void UploadInternalCalls(System.Collections.Generic.Dictionary Date: Sat, 6 Jan 2024 20:38:00 +0100 Subject: [PATCH 07/14] Support for (ReadOnly)Span, (ReadOnly)Memory and List --- .../SourceGenerationHelper.cs | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs index 392803b..8916d4c 100644 --- a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs +++ b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs @@ -41,7 +41,7 @@ public class {{AttributeName}}(InternalCallOptions options = InternalCallOptions public class {{WideStringAttributeName}} : System.Attribute; """; - private static IMethodSymbol _currentMethodSymbol; + private static IMethodSymbol? _currentMethodSymbol; public static string GenerateSource(List internalCalls, SourceProductionContext context) { @@ -137,6 +137,7 @@ public static void UploadInternalCalls(System.Collections.Generic.Dictionary": + case "System.Span<>": + var (spanType, _) = TransformType(namedType.TypeArguments[0], context); + return (spanType + "*", TypeConversionKind.Span); + case "System.ReadOnlyMemory<>": + case "System.Memory<>": + var (memoryType, _) = TransformType(namedType.TypeArguments[0], context); + return (memoryType + "*", TypeConversionKind.Memory); + case "System.Collections.Generic.List<>": + var (listType, _) = TransformType(namedType.TypeArguments[0], context); + return (listType + "*", TypeConversionKind.List); + } + } + + if (!namedType.IsValueType) + { + ReportError(context, "ICG001A", "Reference types are not supported"); return (null, TypeConversionKind.None); } @@ -360,7 +395,7 @@ private static TransformedType TransformReturn(IMethodSymbol method, SourceProdu if (namedType.IsGenericType || !namedType.IsValueType) { - ReportError(context, "ICG001", "Generic types and reference types are not supported"); + ReportError(context, "ICG001B", "Generic and reference return types are not supported"); return (null, TypeConversionKind.None); } @@ -436,7 +471,7 @@ private static void ReportError(SourceProductionContext context, string id, stri DiagnosticSeverity.Error, true ), - (method ?? _currentMethodSymbol).Locations[0] + (method ?? _currentMethodSymbol)!.Locations[0] ) ); } @@ -448,5 +483,8 @@ internal enum TypeConversionKind Array = 1, RefOut = 2, String = 3, - WideString = 4 + WideString = 4, + Span = 5, + List = 6, + Memory = 7 } From 29acf37f8ddddaaaa7c6680c444f001f5590b76d Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 6 Jan 2024 20:45:45 +0100 Subject: [PATCH 08/14] Tests for InternalCalls --- Examples/ExperimentalTesting/InternalCalls.cs | 49 ++++++++++++++++--- Examples/ExperimentalTesting/Plugin.cs | 17 +++++-- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/Examples/ExperimentalTesting/InternalCalls.cs b/Examples/ExperimentalTesting/InternalCalls.cs index e48d1a8..8d8fc84 100644 --- a/Examples/ExperimentalTesting/InternalCalls.cs +++ b/Examples/ExperimentalTesting/InternalCalls.cs @@ -4,8 +4,14 @@ namespace ExperimentalTesting; +public struct HasRefTypes +{ + public string str; + public MtVector2 vec; +} + [InternalCallManager] -public unsafe partial class InternalCalls +public partial class InternalCalls { // Test with simple primitive types [InternalCall] @@ -13,15 +19,15 @@ public unsafe partial class InternalCalls // Test with single pointer type [InternalCall] - public static partial void TestPointers(double* values); + public static unsafe partial void TestPointers(double* values); // Test with mixed unmanaged types [InternalCall] - public static partial char ProcessData(byte* data, int length, ref ushort dataId); + public static unsafe partial char ProcessData(byte* data, int length, ref ushort dataId); // Test with array and pointer types [InternalCall] - public static partial void TransformCoordinates(float[] coords, MtVector3* transformMatrix); + public static unsafe partial void TransformCoordinates(float[] coords, MtVector3* transformMatrix); // Test with InternalCallOptions and ref parameter [InternalCall(InternalCallOptions.Unsafe)] @@ -29,7 +35,7 @@ public unsafe partial class InternalCalls // Test with no return value and primitive parameters [InternalCall] - public static partial void LogMessage(int level, char* message); + public static unsafe partial void LogMessage(int level, char* message); // Test with complex struct return type and primitive parameters [InternalCall] @@ -37,7 +43,7 @@ public unsafe partial class InternalCalls // Test with pointer to struct and primitive types [InternalCall] - public static partial MtVector2* CreateVector(int x, int y); + public static unsafe partial MtVector2* CreateVector(int x, int y); // Test with multiple ref and out parameters [InternalCall] @@ -45,5 +51,34 @@ public unsafe partial class InternalCalls // Test with mixed array, pointer, and primitive types [InternalCall] - public static partial void ProcessBuffers(byte[] inputBuffer, float* outputBuffer, int bufferSize); + public static unsafe partial void ProcessBuffers(byte[] inputBuffer, float* outputBuffer, int bufferSize); + + // Test with strings + [InternalCall] + public static partial MtVector2 Parse(string str); + + [InternalCall] + [return: WideString] + public static partial string Format(MtVector2 vec, [WideString] string str); + + [InternalCall] + public static partial void EnumTest(PropType type); + + [InternalCall] + public static partial void StructTest(HasRefTypes type); + + [InternalCall] + public static partial void SpanTest(Span span); + + [InternalCall] + public static partial void ReadOnlySpanTest(ReadOnlySpan span); + + [InternalCall] + public static partial void MemoryTest(Memory memory); + + [InternalCall] + public static partial void ReadOnlyMemoryTest(ReadOnlyMemory memory); + + [InternalCall] + public static partial void ListTest(List list); } diff --git a/Examples/ExperimentalTesting/Plugin.cs b/Examples/ExperimentalTesting/Plugin.cs index 1c9037b..834e348 100644 --- a/Examples/ExperimentalTesting/Plugin.cs +++ b/Examples/ExperimentalTesting/Plugin.cs @@ -1,4 +1,9 @@ -using SharpPluginLoader.Core; +using System.Collections; +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SharpPluginLoader.Core; using SharpPluginLoader.Core.Entities; using SharpPluginLoader.Core.Experimental; using SharpPluginLoader.Core.Resources; @@ -13,14 +18,18 @@ public class Plugin : IPlugin public delegate void ReleaseResourceDelegate(MtObject resourceMgr, Resource resource); private MarshallingHook _createShinyDropHook = null!; private MarshallingHook _releaseResourceHook = null!; - - public void CreateShinyDropHook(MtObject a, int b, int c, nint d, long e, uint f) + private int _intRef = 0; + public ref int GetIntRef() + { + return ref _intRef; + } + public unsafe void CreateShinyDropHook(MtObject a, int b, int c, nint d, long e, uint f) { Log.Info($"CreateShinyDropHook: {a},{b},{c},{d},{e},{f}"); _createShinyDropHook.Original(a, b, c, d, e, f); } - public void ReleaseResourceHook(MtObject resourceMgr, Resource resource) + public unsafe void ReleaseResourceHook(MtObject resourceMgr, Resource resource) { Log.Info($"Releasing Resource: {resource.FilePath}.{resource.FileExtension}" + (resource.Get(0x5C) == 1 ? " | Unloading..." : "")); From da45163a175fefd15168ea8ea2f5f703c6c8e357 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 7 Jan 2024 12:01:55 +0100 Subject: [PATCH 09/14] Working implementation of internal calls --- .../Memory/Windows/WinApi.cs | 10 ++ SharpPluginLoader.Core/PluginManager.cs | 95 ++++++++++++++++++- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/SharpPluginLoader.Core/Memory/Windows/WinApi.cs b/SharpPluginLoader.Core/Memory/Windows/WinApi.cs index a858a2c..f966118 100644 --- a/SharpPluginLoader.Core/Memory/Windows/WinApi.cs +++ b/SharpPluginLoader.Core/Memory/Windows/WinApi.cs @@ -19,6 +19,16 @@ internal partial class WinApi [LibraryImport("kernel32.dll")] public static partial ulong VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, ulong dwLength); + [LibraryImport("kernel32.dll", EntryPoint = "LoadLibraryW")] + public static partial nint LoadLibrary([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); + + [LibraryImport("kernel32.dll", EntryPoint = "FreeLibrary")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool FreeLibrary(nint hModule); + + [LibraryImport("kernel32.dll", EntryPoint = "GetProcAddress")] + public static partial nint GetProcAddress(nint hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName); + public static bool IsManagedAssembly(string path) { using var stream = File.OpenRead(path); diff --git a/SharpPluginLoader.Core/PluginManager.cs b/SharpPluginLoader.Core/PluginManager.cs index ffa7c81..da04faa 100644 --- a/SharpPluginLoader.Core/PluginManager.cs +++ b/SharpPluginLoader.Core/PluginManager.cs @@ -18,22 +18,27 @@ private class PluginContext public required IPlugin Plugin { get; init; } public required PluginData Data { get; init; } public required string Path {get; init; } + public required nint NativePlugin { get; init; } public void Dispose() { - ConfigManager.SaveAndUnloadConfig(Plugin); Plugin.Dispose(); + if (NativePlugin != 0) + WinApi.FreeLibrary(NativePlugin); + + ConfigManager.SaveAndUnloadConfig(Plugin); Context.Unload(); } } + private delegate void UploadInternalCallsDelegate(Dictionary icalls); private static readonly TimeSpan EventCooldown = TimeSpan.FromMilliseconds(500); - private readonly Dictionary _contexts = new(); + private readonly Dictionary _contexts = []; private readonly FileSystemWatcher _watcher; private readonly object _lock = new(); - private readonly Dictionary _lastEventTimes = new(); + private readonly Dictionary _lastEventTimes = []; #if DEBUG - private readonly List _symlinkWatchers = new(); + private readonly List _symlinkWatchers = []; #endif public PluginManager() @@ -290,6 +295,8 @@ public void LoadPlugin(string pluginPath) var pluginData = plugin.Initialize(); + var nativePlugin = TryLoadNativePlugin(assembly, plugin, Path.ChangeExtension(absPath, ".Native.dll")); + lock (_contexts) { _contexts.Add(plugin.Key, new PluginContext @@ -298,13 +305,91 @@ public void LoadPlugin(string pluginPath) Assembly = assembly, Plugin = plugin, Data = pluginData, - Path = absPath + Path = absPath, + NativePlugin = nativePlugin }); } return; } + private static unsafe nint TryLoadNativePlugin(Assembly assembly, IPlugin plugin, string path) + { + if (!File.Exists(path)) + return 0; + + var nativePlugin = WinApi.LoadLibrary(path); + if (nativePlugin == 0) + { + Log.Error($"Failed to load native plugin for {plugin.Name}"); + return 0; + } + + var getIcallCount = new NativeFunction(WinApi.GetProcAddress(nativePlugin, "get_internal_call_count")); + var collectIcalls = new NativeAction(WinApi.GetProcAddress(nativePlugin, "collect_internal_calls")); + + if (getIcallCount.NativePointer == 0) + { + Log.Error($"Native plugin for {plugin.Name} does not export GetInternalCallCount"); + WinApi.FreeLibrary(nativePlugin); + + return 0; + } + + if (collectIcalls.NativePointer == 0) + { + Log.Error($"Native plugin for {plugin.Name} does not export CollectInternalCalls"); + WinApi.FreeLibrary(nativePlugin); + + return 0; + } + + var icallCount = getIcallCount.Invoke(); + var icalls = NativeArray.Create(icallCount); + + collectIcalls.Invoke(icalls.Address); + + var icallMap = icalls.ToDictionary( + icall => icall.Name, + icall => icall.FunctionPointer + ); + + var icallManager = assembly.GetTypes() + .FirstOrDefault(type => type.GetCustomAttribute() is not null); + if (icallManager is null) + { + Log.Error($"{plugin.Name} has a native component but no InternalCallManager"); + WinApi.FreeLibrary(nativePlugin); + + return 0; + } + + var uploadInternalCalls = icallManager + .GetMethod("UploadInternalCalls", BindingFlags.Public | BindingFlags.Static); + if (uploadInternalCalls is null) + { + Log.Error($"{plugin.Name} has an InternalCallManager but is missing an UploadInternalCalls method"); + WinApi.FreeLibrary(nativePlugin); + + return 0; + } + + try + { + uploadInternalCalls.CreateDelegate()(icallMap); + } + catch (Exception e) + { + Log.Error($"Failed to upload InternalCalls for {plugin.Name}: {e}"); + WinApi.FreeLibrary(nativePlugin); + + return 0; + } + + + return nativePlugin; + } + public IPlugin[] GetPlugins() { lock (_contexts) From e5cd2d8c73fc7fd17af469348c6d7c2e65173764 Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 7 Jan 2024 12:02:19 +0100 Subject: [PATCH 10/14] InternalCall example --- .../ExperimentalTesting.Native.vcxproj | 146 ++++++++++++++++++ ...ExperimentalTesting.Native.vcxproj.filters | 22 +++ .../ExperimentalTesting.Native/dllmain.cpp | 37 +++++ Examples/ExperimentalTesting/InternalCalls.cs | 4 +- Examples/ExperimentalTesting/Plugin.cs | 39 ++++- mhw-cs-plugin-loader.sln | 27 ++++ 6 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj create mode 100644 Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj.filters create mode 100644 Examples/ExperimentalTesting.Native/dllmain.cpp diff --git a/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj new file mode 100644 index 0000000..fcd7e63 --- /dev/null +++ b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj @@ -0,0 +1,146 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {e3ba4f2c-e79f-486e-8051-0835d8057c7d} + ExperimentalTestingNative + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)Examples\ExperimentalTesting\bin\$(Configuration)\net8.0\ + + + $(SolutionDir)Examples\ExperimentalTesting\bin\$(Configuration)\net8.0\ + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + + + Console + true + true + true + + + + + + + + + \ No newline at end of file diff --git a/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj.filters b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj.filters new file mode 100644 index 0000000..e86417e --- /dev/null +++ b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/Examples/ExperimentalTesting.Native/dllmain.cpp b/Examples/ExperimentalTesting.Native/dllmain.cpp new file mode 100644 index 0000000..3e15a80 --- /dev/null +++ b/Examples/ExperimentalTesting.Native/dllmain.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +struct InternalCall { + const char* Name; + void* Function; +}; + +int64_t sum(int64_t a, int64_t b) { + return a + b; +} + +void modify_value(int* ptr) { + *ptr *= 2; +} + +void log_message(int level, const wchar_t* message) { + wchar_t buf[1024]; + (void)swprintf_s(buf, L"LogLevel: %d", level); + MessageBoxW(nullptr, message, buf, MB_OK); +} + +extern "C" __declspec(dllexport) int get_internal_call_count() { + return 3; +} + +extern "C" __declspec(dllexport) void collect_internal_calls(InternalCall * icalls) { + icalls[0] = { "Sum", (void*)sum }; + icalls[1] = { "ModifyValue", (void*)modify_value }; + icalls[2] = { "LogMessage", (void*)log_message }; +} + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) { + return TRUE; +} + diff --git a/Examples/ExperimentalTesting/InternalCalls.cs b/Examples/ExperimentalTesting/InternalCalls.cs index 8d8fc84..e536efa 100644 --- a/Examples/ExperimentalTesting/InternalCalls.cs +++ b/Examples/ExperimentalTesting/InternalCalls.cs @@ -14,7 +14,7 @@ public struct HasRefTypes public partial class InternalCalls { // Test with simple primitive types - [InternalCall] + [InternalCall(InternalCallOptions.Unsafe)] public static partial long Sum(long a, long b); // Test with single pointer type @@ -35,7 +35,7 @@ public partial class InternalCalls // Test with no return value and primitive parameters [InternalCall] - public static unsafe partial void LogMessage(int level, char* message); + public static unsafe partial void LogMessage(int level, [WideString] string message); // Test with complex struct return type and primitive parameters [InternalCall] diff --git a/Examples/ExperimentalTesting/Plugin.cs b/Examples/ExperimentalTesting/Plugin.cs index 834e348..fbfef4c 100644 --- a/Examples/ExperimentalTesting/Plugin.cs +++ b/Examples/ExperimentalTesting/Plugin.cs @@ -3,9 +3,11 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using ImGuiNET; using SharpPluginLoader.Core; using SharpPluginLoader.Core.Entities; using SharpPluginLoader.Core.Experimental; +using SharpPluginLoader.Core.Rendering; using SharpPluginLoader.Core.Resources; namespace ExperimentalTesting @@ -18,11 +20,10 @@ public class Plugin : IPlugin public delegate void ReleaseResourceDelegate(MtObject resourceMgr, Resource resource); private MarshallingHook _createShinyDropHook = null!; private MarshallingHook _releaseResourceHook = null!; - private int _intRef = 0; - public ref int GetIntRef() - { - return ref _intRef; - } + private int _value = 5; + private string _str = "Hello World!"; + private long _a, _b, _sum; + public unsafe void CreateShinyDropHook(MtObject a, int b, int c, nint d, long e, uint f) { Log.Info($"CreateShinyDropHook: {a},{b},{c},{d},{e},{f}"); @@ -38,7 +39,10 @@ public unsafe void ReleaseResourceHook(MtObject resourceMgr, Resource resource) public PluginData Initialize() { - return new PluginData(); + return new PluginData + { + OnImGuiRender = true + }; } public void OnLoad() @@ -46,5 +50,28 @@ public void OnLoad() //_createShinyDropHook = MarshallingHook.Create(CreateShinyDropHook, 0x1402cb1d0); //_releaseResourceHook = MarshallingHook.Create(ReleaseResourceHook, 0x142224890); } + + public void OnImGuiRender() + { + ImGui.InputText("Message", ref _str, 512); + ImGui.SameLine(); + if (ImGui.Button("Log")) + InternalCalls.LogMessage(1, _str); + + ImGui.Separator(); + + ImGuiExtensions.InputScalar("A", ref _a); + ImGui.SameLine(); + ImGuiExtensions.InputScalar("B", ref _b); + ImGui.SameLine(); + ImGui.Text($"Sum: {InternalCalls.Sum(_a, _b)}"); + + ImGui.Separator(); + + ImGui.InputInt("Value", ref _value); + ImGui.SameLine(); + if (ImGui.Button("Modify")) + InternalCalls.ModifyValue(ref _value); + } } } diff --git a/mhw-cs-plugin-loader.sln b/mhw-cs-plugin-loader.sln index a3f3313..b1536cf 100644 --- a/mhw-cs-plugin-loader.sln +++ b/mhw-cs-plugin-loader.sln @@ -35,6 +35,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PreloadTesting", "Examples\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpPluginLoader.InternalCallGenerator", "SharpPluginLoader.InternalCallGenerator\SharpPluginLoader.InternalCallGenerator.csproj", "{CBD13575-CA0C-4EB5-821D-847A6EE5581F}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ExperimentalTesting.Native", "Examples\ExperimentalTesting.Native\ExperimentalTesting.Native.vcxproj", "{E3BA4F2C-E79F-486E-8051-0835D8057C7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -339,6 +341,30 @@ Global {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {CBD13575-CA0C-4EB5-821D-847A6EE5581F}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|Any CPU.ActiveCfg = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|Any CPU.Build.0 = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|x64.ActiveCfg = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|x64.Build.0 = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|x86.ActiveCfg = Debug|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Debug|x86.Build.0 = Debug|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|Any CPU.ActiveCfg = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|Any CPU.Build.0 = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|x64.ActiveCfg = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|x64.Build.0 = Debug|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|x86.ActiveCfg = Debug|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.MinSizeRel|x86.Build.0 = Debug|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|Any CPU.ActiveCfg = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|Any CPU.Build.0 = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|x64.ActiveCfg = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|x64.Build.0 = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|x86.ActiveCfg = Release|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.Release|x86.Build.0 = Release|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|Any CPU.ActiveCfg = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|Any CPU.Build.0 = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|x64.ActiveCfg = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|x64.Build.0 = Release|x64 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|x86.ActiveCfg = Release|Win32 + {E3BA4F2C-E79F-486E-8051-0835D8057C7D}.RelWithDebInfo|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -356,6 +382,7 @@ Global {444FF57A-C89D-403C-ABFB-2CC6BA1D9FCE} = {93226706-71AC-41BC-A457-21D3CAA8C751} {C37E5763-6EBE-45CE-A903-DEF1AEA64FEA} = {93226706-71AC-41BC-A457-21D3CAA8C751} {CBD13575-CA0C-4EB5-821D-847A6EE5581F} = {22614599-FA47-4E69-8DBB-A183F0A4AE25} + {E3BA4F2C-E79F-486E-8051-0835D8057C7D} = {93226706-71AC-41BC-A457-21D3CAA8C751} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {623F8CBC-2C3B-488D-B7BB-0140A8201BB7} From ad8d7ba50c337551c724242d6c0c8b1342ba206b Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sun, 7 Jan 2024 16:56:10 +0100 Subject: [PATCH 11/14] Missing internal call results in a warning not an error --- .../SourceGenerationHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs index 8916d4c..d862db2 100644 --- a/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs +++ b/SharpPluginLoader.InternalCallGenerator/SourceGenerationHelper.cs @@ -271,7 +271,7 @@ public static void UploadInternalCalls(System.Collections.Generic.Dictionary Date: Thu, 11 Jan 2024 20:06:40 +0100 Subject: [PATCH 12/14] InternalCall header --- .../ExperimentalTesting.Native.vcxproj | 3 +++ .../ExperimentalTesting.Native/SPL/InternalCall.h | 14 ++++++++++++++ Examples/ExperimentalTesting.Native/dllmain.cpp | 11 +++++------ 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 Examples/ExperimentalTesting.Native/SPL/InternalCall.h diff --git a/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj index fcd7e63..b732e45 100644 --- a/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj +++ b/Examples/ExperimentalTesting.Native/ExperimentalTesting.Native.vcxproj @@ -140,6 +140,9 @@ + + + diff --git a/Examples/ExperimentalTesting.Native/SPL/InternalCall.h b/Examples/ExperimentalTesting.Native/SPL/InternalCall.h new file mode 100644 index 0000000..f6bcc19 --- /dev/null +++ b/Examples/ExperimentalTesting.Native/SPL/InternalCall.h @@ -0,0 +1,14 @@ +#pragma once + +#ifndef SPL_INTERNAL_CALL +#define SPL_INTERNAL_CALL extern "C" __declspec(dllexport) +#endif + +namespace SharpPluginLoader::Native { + +struct InternalCall { + const char* Name; + void* Function; +}; + +} \ No newline at end of file diff --git a/Examples/ExperimentalTesting.Native/dllmain.cpp b/Examples/ExperimentalTesting.Native/dllmain.cpp index 3e15a80..d047790 100644 --- a/Examples/ExperimentalTesting.Native/dllmain.cpp +++ b/Examples/ExperimentalTesting.Native/dllmain.cpp @@ -2,10 +2,9 @@ #include #include -struct InternalCall { - const char* Name; - void* Function; -}; +#include "SPL/InternalCall.h" + +namespace SPLNative = SharpPluginLoader::Native; int64_t sum(int64_t a, int64_t b) { return a + b; @@ -21,11 +20,11 @@ void log_message(int level, const wchar_t* message) { MessageBoxW(nullptr, message, buf, MB_OK); } -extern "C" __declspec(dllexport) int get_internal_call_count() { +SPL_INTERNAL_CALL int get_internal_call_count() { return 3; } -extern "C" __declspec(dllexport) void collect_internal_calls(InternalCall * icalls) { +SPL_INTERNAL_CALL void collect_internal_calls(SPLNative::InternalCall* icalls) { icalls[0] = { "Sum", (void*)sum }; icalls[1] = { "ModifyValue", (void*)modify_value }; icalls[2] = { "LogMessage", (void*)log_message }; From 75344a794c42edad93bdbe2269bf767180e1afcc Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 13 Jan 2024 13:48:35 +0100 Subject: [PATCH 13/14] InternalCalls/Native Components documentation --- docs/Development/NativeComponents.md | 84 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 85 insertions(+) create mode 100644 docs/Development/NativeComponents.md diff --git a/docs/Development/NativeComponents.md b/docs/Development/NativeComponents.md new file mode 100644 index 0000000..f477b42 --- /dev/null +++ b/docs/Development/NativeComponents.md @@ -0,0 +1,84 @@ +# Native Components +Generally, most plugin-related things should be achievable in C# with the functionality provided by the framework. But sometimes it is perhaps easier to implement in C/C++ than in C#. Or maybe there is a C++ library that you want to use from your plugin. + +This is where Native Components come in. A native component is an unmanaged counterpart to a C# plugin. Usually when you need to interop with native code in C# you do so using [P/Invoke](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke). However the framework provides a more performant and versatile alternative to P/Invoke in the form of Internal Calls. + +Every plugin `PluginName.dll` can have a native component `PluginName.Native.dll`, which is required to be a native dll. Native components need to export 2 functions: +```cpp +#include "SPL/InternalCall.h" + +namespace SPLNative = SharpPluginLoader::Native; + +SPL_INTERNAL_CALL int get_internal_call_count() { + // ... +} + +SPL_INTERNAL_CALL void collect_internal_calls(SPLNative::InternalCall* icalls) { + // ... +} +``` + +`get_internal_call_count` tells the framework how many internal calls are exported by the native component. + +In the `collect_internal_calls` function the native component then fills out the `icalls` array. + +For example, say you have some ImGui widget implemented in C++ that you want to expose to your plugin, your native component might look something like this: +```cpp +... +bool my_imgui_widget(const char* label, int* values, unsigned flags) { ... } + +SPL_INTERNAL_CALL int get_internal_call_count() { return 1; } +SPL_INTERNAL_CALL void collect_internal_calls(SPLNative::InternalCall* icalls) { + icalls[0] = { "MyImGuiWidget", &my_imgui_widget }; +} +``` + +The important part here is this line: +```cpp +icalls[0] = { "MyImGuiWidget", &my_imgui_widget }; +``` + +The first component here is the name of the internal call on the C# side, which must match exactly. The second component is the function pointer. + +Now on the C# side you need to first add a reference to the `SharpPluginLoader.InternalCallGenerator` NuGet package to your plugin. + +Next you need to designate one class in your plugin as the internal call manager. Typically you would create a class dedicated to holding all of the internal calls. Technically however, you can make any class (even your main plugin class) the internal call manager. What is important though is that there can only be *one* internal call manager per plugin. + +```cs +using SharpPluginLoader.InternalCallGenerator; + +[InternalCallManager] +public partial class InternalCalls +{ + [InternalCall] + public static partial bool MyImGuiWidget(string label, Span values, uint flags); +} +``` + +The internal call manager is marked using the `[InternalCallManager]` attribute. Each internal call is then marked with an `[InternalCall]` attribute. It is required that the internal call manager class is `partial`. The same goes for all internal call methods, which must be marked `static partial`. + +This is all the code required on the managed side. Now you can call your native function by doing `InternalCalls.MyImGuiWidget("Test", [3, 4, 5]);`. + +Of course you can also bind an internal call to a game function directly: +```cpp +icalls[2] = { "SomeGameFunction", (void*)0x1430ae620 }; +``` + +As you can see the managed function takes a `string` and a `Span` as parameters. This works because the InternalCallGenerator will generate marshalling code for these types behind the scenes. Below is a list of managed types and what they map to on the native end: + +| Managed Type | Native Type | Copy Required | Remarks | +| ------------ | ----------- | ------------- | ------- | +| `string` | `char*` / `char8_t*` | Yes | UTF8 Encoding | +| `[WideString] string` | `wchar_t*` / `char16_t*` | Yes | UTF16 Encoding | +| `T[]` | `T*` | No | `T` must be unmanaged | +| `T` | `T` | Yes | ^ | +| `List` | `T*` | No | ^ | +| `{ReadOnly}Span` | `T*` | No | ^ | +| `{ReadOnly}Memory` | `T*` | No | ^ | +| `ref/out T` | `T*` | No | ^ | +| `struct` | `struct` | Yes | The struct must be unmanaged (i.e. contain no reference types) | +| `ref/out struct` | `struct*` | No | ^ | +| `class` | Unsupported | | | + +## Other Languages +It is also possible to write native components in languages other than C++ such as Rust. The only requirement is that the dll uses the [Microsoft x64 calling convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170) for the two exported functions. This is the default calling convention when compiling C/C++ with MSVC on x64. diff --git a/mkdocs.yml b/mkdocs.yml index 661e9a4..49da8ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,7 @@ nav: - Calling Native Functions: Development/CallingNativeFuncs.md - Rendering with ImGui: Development/ImGui.md - Rendering: Development/Rendering.md + - Native Components: Development/NativeComponents.md - API Reference: - API/index.md - Examples: From 9c722510ccb26acf35addf6e823450f9b6584afa Mon Sep 17 00:00:00 2001 From: Fexty12573 Date: Sat, 13 Jan 2024 14:29:19 +0100 Subject: [PATCH 14/14] Nuget package configuration --- SharpPluginLoader.InternalCallGenerator/README.md | 5 +++++ .../SharpPluginLoader.InternalCallGenerator.csproj | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 SharpPluginLoader.InternalCallGenerator/README.md diff --git a/SharpPluginLoader.InternalCallGenerator/README.md b/SharpPluginLoader.InternalCallGenerator/README.md new file mode 100644 index 0000000..9221a21 --- /dev/null +++ b/SharpPluginLoader.InternalCallGenerator/README.md @@ -0,0 +1,5 @@ +# SharpPluginLoader.InternalCallGenerator + +This is a source generator that generates internal calls. It is used in conjunction with [SharpPluginLoader](https://github.com/Fexty12573/SharpPluginLoader). + +See details on internal calls [here](https://fexty12573.github.io/SharpPluginLoader/Development/NativeComponents/). diff --git a/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj b/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj index 27d5bfe..a5158e3 100644 --- a/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj +++ b/SharpPluginLoader.InternalCallGenerator/SharpPluginLoader.InternalCallGenerator.csproj @@ -4,11 +4,18 @@ netstandard2.0 12.0 True - InternalCall Generator + SharpPluginLoader InternalCall Generator false enable true true + True + Fexty,Ando + + A source generator to generate InternalCalls for plugins + https://fexty12573.github.io/SharpPluginLoader/ + $(ProjectDir)README.md + https://github.com/Fexty12573/SharpPluginLoader @@ -21,6 +28,10 @@ + + True + \ +