diff --git a/Source/ExcelDna.Integration/Registration/ParamsRegistration.cs b/Source/ExcelDna.Integration/Registration/ParamsRegistration.cs index 5e0bf48c..450e58f7 100644 --- a/Source/ExcelDna.Integration/Registration/ParamsRegistration.cs +++ b/Source/ExcelDna.Integration/Registration/ParamsRegistration.cs @@ -144,8 +144,8 @@ static LambdaExpression WrapMethodParams(LambdaExpression functionLambda) * */ - int maxArguments = 125; // Constrained by 255 char registration string, take off 3 type chars, use up to 2 chars per param (before we start doing object...) (& also return) - // CONSIDER: Might improve this if we generate the delegate based on the max length... + int maxArguments = NativeAOT.IsActive ? 16 : 125; // Constrained by 255 char registration string, take off 3 type chars, use up to 2 chars per param (before we start doing object...) (& also return) + // CONSIDER: Might improve this if we generate the delegate based on the max length... var normalParams = functionLambda.Parameters.Take(functionLambda.Parameters.Count() - 1).ToList(); var normalParamCount = normalParams.Count; @@ -210,13 +210,19 @@ static LambdaExpression WrapMethodParams(LambdaExpression functionLambda) if (maxArguments == 125) { delegateType = typeof(CustomFunc125<,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>) - .MakeGenericType(allParamTypes.ToArray()); + .MakeGenericType(allParamTypes.ToArray()); } - else // if (maxArguments == 29) + else if (maxArguments == 29) { delegateType = typeof(CustomFunc29<,,,,,,,,,,,,,,,,,,,,,,,,,,,,,>) .MakeGenericType(allParamTypes.ToArray()); } + else // if (maxArguments == 16) + { + delegateType = typeof(Func<,,,,,,,,,,,,,,,,>) + .MakeGenericType(allParamTypes.ToArray()); + } + return Expression.Lambda(delegateType, blockExpr, allParamExprs); } } diff --git a/Source/ExcelDna.SourceGenerator.NativeAOT/Generator.cs b/Source/ExcelDna.SourceGenerator.NativeAOT/Generator.cs index ad6e109a..30f6b816 100644 --- a/Source/ExcelDna.SourceGenerator.NativeAOT/Generator.cs +++ b/Source/ExcelDna.SourceGenerator.NativeAOT/Generator.cs @@ -30,6 +30,9 @@ public void Execute(GeneratorExecutionContext context) string source = """ // +using System; +using System.Collections.Generic; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -44,7 +47,7 @@ public static short Initialize(void* xlAddInExportInfoAddress, void* hModuleXll, [ADDINS] - [FUNCTIONS] +[FUNCTIONS] return ExcelDna.ManagedHost.AddInInitialize.InitializeNativeAOT(xlAddInExportInfoAddress, hModuleXll, pPathXLL, disableAssemblyContextUnload, pTempDirPath); } @@ -68,16 +71,26 @@ public static short Initialize(void* xlAddInExportInfoAddress, void* hModuleXll, source = source.Replace("[ADDINS]", addIns); } { - string functions = "List functionTypes = new List();\r\n"; + string functions = "List typeRefs = new List();\r\n"; + string methods = "List methodRefs = new List();\r\n"; foreach (var i in receiver.Functions) { functions += $"ExcelDna.Integration.NativeAOT.MethodsForRegistration.Add(typeof({Util.GetFullTypeName(i.ContainingType)}).GetMethod(\"{i.Name}\")!);\r\n"; - functions += $"functionTypes.Add(typeof({Util.MethodType(i)}));\r\n"; + functions += $"typeRefs.Add(typeof({Util.MethodType(i)}));\r\n"; foreach (var p in i.Parameters) - functions += $"functionTypes.Add(typeof(Func));\r\n"; + { + functions += $"typeRefs.Add(typeof(Func));\r\n"; + } + + if (i.Parameters.Length > 0 && i.Parameters.Last().IsParams && i.Parameters.Last().Type is IArrayTypeSymbol arrayType) + { + methods += $"methodRefs.Add(typeof(List<{Util.GetFullTypeName(arrayType.ElementType)}>).GetMethod(\"ToArray\")!);\r\n"; + functions += $"typeRefs.Add(typeof(Func<{Util.CreateFunc16Args(i)}>));\r\n"; + } + functions += "\r\n"; } - source = source.Replace("[FUNCTIONS]", functions); + source = source.Replace("[FUNCTIONS]", functions + methods); } context.AddSource($"ExcelDna.SG.NAOT.Init.g.cs", source); diff --git a/Source/ExcelDna.SourceGenerator.NativeAOT/Util.cs b/Source/ExcelDna.SourceGenerator.NativeAOT/Util.cs index 9095bf32..7ce7f8de 100644 --- a/Source/ExcelDna.SourceGenerator.NativeAOT/Util.cs +++ b/Source/ExcelDna.SourceGenerator.NativeAOT/Util.cs @@ -39,6 +39,19 @@ public static string MethodType(IMethodSymbol method) $"Func<{(string.IsNullOrWhiteSpace(parameters) ? null : $"{parameters}, ")}{GetFullTypeName(method.ReturnType)}>"; } + public static string CreateFunc16Args(IMethodSymbol method) + { + List allParamTypes = method.Parameters.Take(method.Parameters.Length - 1).Select(p => p.Type).Cast().ToList(); + var toAdd = 16 - allParamTypes.Count; + for (int i = 0; i < toAdd; i++) + { + allParamTypes.Add(null); + } + allParamTypes.Add(method.ReturnType); + + return string.Join(",", allParamTypes.Select(i => i == null ? "object" : GetFullTypeName(i))); + } + private static SymbolDisplayFormat FullNameFormat = new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); } } diff --git a/Source/ExcelDna.sln b/Source/ExcelDna.sln index 0dd685bf..ca41fab7 100644 --- a/Source/ExcelDna.sln +++ b/Source/ExcelDna.sln @@ -132,6 +132,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExcelDna.AddIn.RuntimeTests {D50C3A8E-46F1-F61B-C8F5-ECDA05995EEB} = {D50C3A8E-46F1-F61B-C8F5-ECDA05995EEB} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExcelDna.SourceGenerator.NativeAOT.Tests", "Tests\ExcelDna.SourceGenerator.NativeAOT.Tests\ExcelDna.SourceGenerator.NativeAOT.Tests.csproj", "{D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -292,6 +294,14 @@ Global {1EC2EE86-7C59-4CA9-9D6A-FBF206DA9D3F}.Release|Win32.ActiveCfg = Release|Any CPU {1EC2EE86-7C59-4CA9-9D6A-FBF206DA9D3F}.Release|x64.ActiveCfg = Release|Any CPU {1EC2EE86-7C59-4CA9-9D6A-FBF206DA9D3F}.Release|x64.Build.0 = Release|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Debug|Win32.ActiveCfg = Debug|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Debug|Win32.Build.0 = Debug|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Debug|x64.ActiveCfg = Debug|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Debug|x64.Build.0 = Debug|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Release|Win32.ActiveCfg = Release|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Release|Win32.Build.0 = Release|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Release|x64.ActiveCfg = Release|Any CPU + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -311,6 +321,7 @@ Global {7CA501E1-FB74-4903-B34D-EFE513C5B428} = {511CE610-5E55-457F-B818-1522D1A96727} {151AE960-6320-402D-BDD0-D76DF96F2BD7} = {511CE610-5E55-457F-B818-1522D1A96727} {1EC2EE86-7C59-4CA9-9D6A-FBF206DA9D3F} = {511CE610-5E55-457F-B818-1522D1A96727} + {D47CC5BF-712E-4FFA-BE52-E783BDCC6D33} = {511CE610-5E55-457F-B818-1522D1A96727} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69EF43B0-B903-4775-BC81-C7E7E012EDA3} diff --git a/Source/Tests/ExcelDna.AddIn.RuntimeTestsAOT/Functions.cs b/Source/Tests/ExcelDna.AddIn.RuntimeTestsAOT/Functions.cs index 13ddfa5e..e44678ce 100644 --- a/Source/Tests/ExcelDna.AddIn.RuntimeTestsAOT/Functions.cs +++ b/Source/Tests/ExcelDna.AddIn.RuntimeTestsAOT/Functions.cs @@ -106,5 +106,73 @@ public static string NativeRangeAddress(IRange r) { return "Native Address: " + r.Get("Address"); } + + [ExcelFunction] + public static string NativeEnum(DateTimeKind e) + { + return "Native Enum VAL: " + e.ToString(); + } + + [ExcelFunction] + public static DateTimeKind NativeEnumReturn(string s) + { + return Enum.Parse(s); + } + + [ExcelFunction] + public static string NativeStringArray(string[] s) + { + return "Native StringArray VALS: " + string.Concat(s); + } + + [ExcelFunction] + public static string NativeStringArray2D(string[,] s) + { + string result = ""; + for (int i = 0; i < s.GetLength(0); i++) + { + for (int j = 0; j < s.GetLength(1); j++) + { + result += s[i, j]; + } + + result += " "; + } + + return $"Native StringArray2D VALS: {result}"; + } + + [ExcelFunction] + public static string NativeParamsFunc1( + [ExcelArgument(Name = "first.Input", Description = "is a useful start")] + object input, + [ExcelArgument(Description = "is another param start")] + string QtherInpEt, + [ExcelArgument(Name = "Value", Description = "gives the Rest")] + params object[] args) + { + return input + "," + QtherInpEt + ", : " + args.Length; + } + + [ExcelFunction] + public static string NativeParamsFunc2( + [ExcelArgument(Name = "first.Input", Description = "is a useful start")] + object input, + [ExcelArgument(Name = "second.Input", Description = "is some more stuff")] + string input2, + [ExcelArgument(Description = "is another param ")] + string QtherInpEt, + [ExcelArgument(Name = "Value", Description = "gives the Rest")] + params object[] args) + { + var content = string.Join(",", args.Select(ValueType => ValueType.ToString())); + return input + "," + input2 + "," + QtherInpEt + ", " + $"[{args.Length}: {content}]"; + } + + [ExcelFunction] + public static string NativeParamsJoinString(string separator, params string[] values) + { + return String.Join(separator, values); + } } } diff --git a/Source/Tests/ExcelDna.RuntimeTests/NativeAOT.cs b/Source/Tests/ExcelDna.RuntimeTests/NativeAOT.cs index ec881a86..4306a037 100644 --- a/Source/Tests/ExcelDna.RuntimeTests/NativeAOT.cs +++ b/Source/Tests/ExcelDna.RuntimeTests/NativeAOT.cs @@ -139,5 +139,95 @@ public void Range() functionRange3.Formula = "=NativeRangeAddress((B2,D5:E6))"; Assert.Equal("Native Address: $B$2,$D$5:$E$6", functionRange3.Value.ToString()); } + + [ExcelFact(Workbook = "", AddIn = AddInPath.RuntimeTestsAOT)] + public void Enum() + { + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B1:B1"]; + functionRange.Formula = "=NativeEnum(\"Unspecified\")"; + Assert.Equal("Native Enum VAL: Unspecified", functionRange.Value.ToString()); + + functionRange.Formula = "=NativeEnum(\"Local\")"; + Assert.Equal("Native Enum VAL: Local", functionRange.Value.ToString()); + + functionRange.Formula = "=NativeEnum(1)"; + Assert.Equal("Native Enum VAL: Utc", functionRange.Value.ToString()); + } + + [ExcelFact(Workbook = "", AddIn = AddInPath.RuntimeTestsAOT)] + public void EnumReturn() + { + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B1:B1"]; + functionRange.Formula = "=NativeEnumReturn(\"Unspecified\")"; + Assert.Equal("Unspecified", functionRange.Value.ToString()); + + functionRange.Formula = "=NativeEnumReturn(\"Local\")"; + Assert.Equal("Local", functionRange.Value.ToString()); + } + + [ExcelFact(Workbook = "", AddIn = AddInPath.RuntimeTestsAOT)] + public void StringArray() + { + Range a1 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A1:A1"]; + a1.Value = "01"; + + Range a2 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A2:A2"]; + a2.Value = "2.30"; + + Range a3 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A3:A3"]; + a3.Value = "World"; + + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B1:B1"]; + functionRange.Formula = "=NativeStringArray(A1:A3)"; + + Assert.Equal("Native StringArray VALS: 12.3World", functionRange.Value.ToString()); + } + + [ExcelFact(Workbook = "", AddIn = AddInPath.RuntimeTestsAOT)] + public void StringArray2D() + { + Range a1 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A1"]; + a1.Value = "01"; + + Range a2 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A2"]; + a2.Value = "2.30"; + + Range a3 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["A3"]; + a3.Value = "Hello"; + + Range b1 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B1"]; + b1.Value = "5"; + + Range b2 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B2"]; + b2.Value = "6.7"; + + Range b3 = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B3"]; + b3.Value = "World"; + + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["C1"]; + functionRange.Formula = "=NativeStringArray2D(A1:B3)"; + + Assert.Equal("Native StringArray2D VALS: 15 2.36.7 HelloWorld ", functionRange.Value.ToString()); + } + + [ExcelFact(Workbook = "", AddIn = AddInPath.RuntimeTestsAOT)] + public void Params() + { + { + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B1"]; + functionRange.Formula = "=NativeParamsFunc1(1,\"2\",4,5)"; + Assert.Equal("1,2, : 2", functionRange.Value.ToString()); + } + { + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B2"]; + functionRange.Formula = "=NativeParamsFunc2(\"a\",,\"c\",\"d\",,\"f\")"; + Assert.Equal("a,,c, [3: d,ExcelDna.Integration.ExcelMissing,f]", functionRange.Value.ToString()); + } + { + Range functionRange = ((Worksheet)ExcelDna.Testing.Util.Workbook.Sheets[1]).Range["B3"]; + functionRange.Formula = "=NativeParamsJoinString(\"//\",\"5\",\"4\",\"3\")"; + Assert.Equal("5//4//3", functionRange.Value.ToString()); + } + } } } diff --git a/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/ExcelDna.SourceGenerator.NativeAOT.Tests.csproj b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/ExcelDna.SourceGenerator.NativeAOT.Tests.csproj new file mode 100644 index 00000000..de666dd1 --- /dev/null +++ b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/ExcelDna.SourceGenerator.NativeAOT.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0-windows + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/Generator.cs b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/Generator.cs new file mode 100644 index 00000000..b16c9145 --- /dev/null +++ b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/Generator.cs @@ -0,0 +1,76 @@ +namespace ExcelDna.SourceGenerator.NativeAOT.Tests +{ + public class Generator + { + [Fact] + public void Empty() + { + VerifyFunctions("", """ + List typeRefs = new List(); + List methodRefs = new List(); + """); + } + + [Fact] + public void Params() + { + VerifyFunctions(""" + using ExcelDna.Integration; + + namespace ExcelDna.AddIn.RuntimeTestsAOT + { + public class Functions + { + [ExcelFunction] + public static string NativeParamsJoinString(string separator, params string[] values) + { + return string.Join(separator, values); + } + } + } + """, """ + List typeRefs = new List(); + ExcelDna.Integration.NativeAOT.MethodsForRegistration.Add(typeof(ExcelDna.AddIn.RuntimeTestsAOT.Functions).GetMethod("NativeParamsJoinString")!); + typeRefs.Add(typeof(Func)); + typeRefs.Add(typeof(Func)); + typeRefs.Add(typeof(Func)); + typeRefs.Add(typeof(Func)); + + List methodRefs = new List(); + methodRefs.Add(typeof(List).GetMethod("ToArray")!); + """); + } + + private static void VerifyFunctions(string sourceCode, string functions) + { + string template = """ + // + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + namespace ExcelDna.SourceGenerator.NativeAOT + { + public unsafe class AddInInitialize + { + [UnmanagedCallersOnly(EntryPoint = "Initialize", CallConvs = new[] { typeof(CallConvCdecl) })] + public static short Initialize(void* xlAddInExportInfoAddress, void* hModuleXll, void* pPathXLL, byte disableAssemblyContextUnload, void* pTempDirPath) + { + ExcelDna.Integration.NativeAOT.IsActive = true; + + + + [FUNCTIONS] + + + return ExcelDna.ManagedHost.AddInInitialize.InitializeNativeAOT(xlAddInExportInfoAddress, hModuleXll, pPathXLL, disableAssemblyContextUnload, pTempDirPath); + } + } + } + """; + SourceGeneratorDriver.Verify(sourceCode, template.Replace("[FUNCTIONS]", functions)); + } + } +} diff --git a/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/SourceGeneratorDriver.cs b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/SourceGeneratorDriver.cs new file mode 100644 index 00000000..900e1882 --- /dev/null +++ b/Source/Tests/ExcelDna.SourceGenerator.NativeAOT.Tests/SourceGeneratorDriver.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace ExcelDna.SourceGenerator.NativeAOT.Tests +{ + internal class SourceGeneratorDriver + { + public static void Verify(string sourceCode, string expected) + { + Compilation inputCompilation = CSharpCompilation.Create("compilation", + [CSharpSyntaxTree.ParseText(sourceCode)], + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(System.Reflection.Assembly.Load("System.Runtime").Location), + MetadataReference.CreateFromFile(System.Reflection.Assembly.Load("System.Collections").Location), + MetadataReference.CreateFromFile(typeof(ExcelDna.Integration.NativeAOT).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ExcelDna.ManagedHost.AddInInitialize).Assembly.Location), + ], + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithAllowUnsafe(true)); + NativeAOT.Generator generator = new NativeAOT.Generator(); + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator).RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics); + + Assert.Empty(diagnostics); + Assert.True(outputCompilation.SyntaxTrees.Count() == 2); + var outputCompilationDiagnostics = outputCompilation.GetDiagnostics(); + Assert.Empty(outputCompilationDiagnostics); + + GeneratorDriverRunResult runResult = driver.GetRunResult(); + Assert.True(runResult.GeneratedTrees.Length == 1); + Assert.Empty(runResult.Diagnostics); + + GeneratorRunResult generatorResult = runResult.Results[0]; + Assert.True(generatorResult.Generator == generator); + Assert.Empty(generatorResult.Diagnostics); + Assert.True(generatorResult.GeneratedSources.Length == 1); + Assert.True(generatorResult.Exception is null); + Assert.Equal(expected, generatorResult.GeneratedSources[0].SourceText.ToString()); + } + } +}