From de8edba8ad12b7952805b86e72814e52625d1f67 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 26 Oct 2020 02:11:24 +0300 Subject: [PATCH] [WIP] IDL-based codegen --- Avalonia.sln | 53 ++ packages/Avalonia/Avalonia.csproj | 7 +- packages/Avalonia/AvaloniaBuildTasks.targets | 2 + src/Avalonia.Build.Tasks/ComInteropHelper.cs | 130 +++++ .../CompileAvaloniaXamlTask.cs | 6 +- src/Avalonia.Build.Tasks/Program.cs | 3 +- .../XamlCompilerTaskExecutor.cs | 8 +- .../Avalonia.MicroCom.csproj | 8 + .../IMicroComExceptionCallback.cs | 9 + .../IMicroComShadowContainer.cs | 9 + src/Avalonia.MicroCom/IUnknown.cs | 12 + src/Avalonia.MicroCom/LocalInterop.cs | 17 + src/Avalonia.MicroCom/MicroComProxyBase.cs | 78 +++ src/Avalonia.MicroCom/MicroComRuntime.cs | 88 +++ src/Avalonia.MicroCom/MicroComShadow.cs | 171 ++++++ src/Avalonia.MicroCom/MicroComVtblBase.cs | 41 ++ src/Avalonia.Native/avn.idl | 522 ++++++++++++++++++ src/tools/MicroComGenerator/Ast.cs | 102 ++++ src/tools/MicroComGenerator/AstParser.cs | 228 ++++++++ .../CSharpGen.InterfaceGen.cs | 453 +++++++++++++++ .../MicroComGenerator/CSharpGen.Utils.cs | 97 ++++ src/tools/MicroComGenerator/CSharpGen.cs | 89 +++ src/tools/MicroComGenerator/CppGen.cs | 108 ++++ src/tools/MicroComGenerator/Extensions.cs | 90 +++ .../MicroComGenerator.csproj | 12 + src/tools/MicroComGenerator/ParseException.cs | 27 + src/tools/MicroComGenerator/Program.cs | 44 ++ src/tools/MicroComGenerator/TokenParser.cs | 417 ++++++++++++++ 28 files changed, 2821 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Build.Tasks/ComInteropHelper.cs create mode 100644 src/Avalonia.MicroCom/Avalonia.MicroCom.csproj create mode 100644 src/Avalonia.MicroCom/IMicroComExceptionCallback.cs create mode 100644 src/Avalonia.MicroCom/IMicroComShadowContainer.cs create mode 100644 src/Avalonia.MicroCom/IUnknown.cs create mode 100644 src/Avalonia.MicroCom/LocalInterop.cs create mode 100644 src/Avalonia.MicroCom/MicroComProxyBase.cs create mode 100644 src/Avalonia.MicroCom/MicroComRuntime.cs create mode 100644 src/Avalonia.MicroCom/MicroComShadow.cs create mode 100644 src/Avalonia.MicroCom/MicroComVtblBase.cs create mode 100644 src/Avalonia.Native/avn.idl create mode 100644 src/tools/MicroComGenerator/Ast.cs create mode 100644 src/tools/MicroComGenerator/AstParser.cs create mode 100644 src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs create mode 100644 src/tools/MicroComGenerator/CSharpGen.Utils.cs create mode 100644 src/tools/MicroComGenerator/CSharpGen.cs create mode 100644 src/tools/MicroComGenerator/CppGen.cs create mode 100644 src/tools/MicroComGenerator/Extensions.cs create mode 100644 src/tools/MicroComGenerator/MicroComGenerator.csproj create mode 100644 src/tools/MicroComGenerator/ParseException.cs create mode 100644 src/tools/MicroComGenerator/Program.cs create mode 100644 src/tools/MicroComGenerator/TokenParser.cs diff --git a/Avalonia.sln b/Avalonia.sln index 34ad19b41d..74a2dbb94b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -226,6 +226,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -2064,6 +2068,54 @@ Global {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhone.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhone.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|Any CPU.Build.0 = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.ActiveCfg = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhone.Build.0 = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|iPhone.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|iPhone.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|Any CPU.Build.0 = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|iPhone.ActiveCfg = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|iPhone.Build.0 = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2123,6 +2175,7 @@ Global {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index cd3ce9adcd..90c1c67956 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,8 +5,9 @@ - + + @@ -15,9 +16,7 @@ - + <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll"> diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 612c368633..57b4cb04e3 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -3,6 +3,7 @@ <_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild) <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false low + <_AvaloniaPatchComInterop Condition="'$(_AvaloniaPatchComInterop)' == ''">false @@ -90,6 +91,7 @@ AssemblyOriginatorKeyFile="$(AssemblyOriginatorKeyFile)" SignAssembly="$(SignAssembly)" DelaySign="$(DelaySign)" + EnableComInteropPatching="$(_AvaloniaPatchComInterop)" /> (); + var initializers = new List(); + foreach (var type in asm.MainModule.Types) + { + var i = type.Methods.FirstOrDefault(m => m.Name == "__MicroComModuleInit"); + if (i != null) + initializers.Add(i); + + PatchType(type, classToRemoveList); + } + + // Remove All Interop classes + foreach (var type in classToRemoveList) + asm.MainModule.Types.Remove(type); + + + // Patch automatic registrations + if (initializers.Count != 0) + { + var moduleType = asm.MainModule.Types.First(x => x.Name == ""); + + // Needed for compatibility with upcoming .NET 5 feature, look for existing initializer first + var staticCtor = moduleType.Methods.FirstOrDefault(m => m.Name == ".cctor"); + if (staticCtor == null) + { + // Create a new static ctor if none exists + staticCtor = new MethodDefinition(".cctor", + MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | + MethodAttributes.Static | MethodAttributes.Private, + asm.MainModule.TypeSystem.Void); + staticCtor.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + moduleType.Methods.Add(staticCtor); + } + + foreach (var i in initializers) + staticCtor.Body.Instructions.Add(Instruction.Create(OpCodes.Call, i)); + } + + } + + + + static void PatchMethod(MethodDefinition method) + { + if (method.HasBody) + { + var ilProcessor = method.Body.GetILProcessor(); + + var instructions = method.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + Instruction instruction = instructions[i]; + + if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference) + { + var methodDescription = (MethodReference)instruction.Operand; + + if (methodDescription.Name.StartsWith("Calli") && methodDescription.DeclaringType.Name == "LocalInterop") + { + var callSite = new CallSite(methodDescription.ReturnType) { CallingConvention = MethodCallingConvention.StdCall }; + + if (methodDescription.Name.StartsWith("CalliCdecl")) + { + callSite.CallingConvention = MethodCallingConvention.C; + } + else if(methodDescription.Name.StartsWith("CalliThisCall")) + { + callSite.CallingConvention = MethodCallingConvention.ThisCall; + } + else if(methodDescription.Name.StartsWith("CalliStdCall")) + { + callSite.CallingConvention = MethodCallingConvention.StdCall; + } + else if(methodDescription.Name.StartsWith("CalliFastCall")) + { + callSite.CallingConvention = MethodCallingConvention.FastCall; + } + + // Last parameter is the function ptr, so we don't add it as a parameter for calli + // as it is already an implicit parameter for calli + for (int j = 0; j < methodDescription.Parameters.Count - 1; j++) + { + var parameterDefinition = methodDescription.Parameters[j]; + callSite.Parameters.Add(parameterDefinition); + } + + // Create calli Instruction + var callIInstruction = ilProcessor.Create(OpCodes.Calli, callSite); + + // Replace instruction + ilProcessor.Replace(instruction, callIInstruction); + } + } + } + } + } + + /// + /// Patches the type. + /// + /// The type. + static void PatchType(TypeDefinition type, List classToRemoveList) + { + // Patch methods + foreach (var method in type.Methods) + PatchMethod(method); + + if (type.Name == "LocalInterop") + classToRemoveList.Add(type); + + // Patch nested types + foreach (var typeDefinition in type.NestedTypes) + PatchType(typeDefinition, classToRemoveList); + } + } +} diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 8e1f6c257d..f44e90b25e 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -40,8 +40,8 @@ namespace Avalonia.Build.Tasks var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), ProjectDirectory, OutputPath, VerifyIl, outputImportance, - (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null - ); + (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, + EnableComInteropPatching); if (!res.Success) return false; if (!res.WrittenFile) @@ -76,6 +76,8 @@ namespace Avalonia.Build.Tasks public bool VerifyIl { get; set; } + public bool EnableComInteropPatching { get; set; } + public string AssemblyOriginatorKeyFile { get; set; } public bool SignAssembly { get; set; } public bool DelaySign { get; set; } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index 1909c4c6ec..b18c19cd63 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -29,7 +29,8 @@ namespace Avalonia.Build.Tasks OutputPath = args[2], BuildEngine = new ConsoleBuildEngine(), ProjectDirectory = Directory.GetCurrentDirectory(), - VerifyIl = true + VerifyIl = true, + EnableComInteropPatching = true }.Execute() ? 0 : 2; diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 6b01af2ede..938732cfe1 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -42,13 +42,13 @@ namespace Avalonia.Build.Tasks } public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory, - string output, bool verifyIl, MessageImportance logImportance, string strongNameKey) + string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom) { var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input); var asm = typeSystem.TargetAssemblyDefinition; var emres = new EmbeddedResources(asm); var avares = new AvaloniaResources(asm, projectDirectory); - if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) + if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0 && !patchCom) // Nothing to do return new CompileResult(true); @@ -374,7 +374,9 @@ namespace Avalonia.Build.Tasks loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull)); loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); - + + if (patchCom) + ComInteropHelper.PatchAssembly(asm, typeSystem); var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; if (!string.IsNullOrWhiteSpace(strongNameKey)) diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj new file mode 100644 index 0000000000..aaf54ed80e --- /dev/null +++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.0 + true + + + diff --git a/src/Avalonia.MicroCom/IMicroComExceptionCallback.cs b/src/Avalonia.MicroCom/IMicroComExceptionCallback.cs new file mode 100644 index 0000000000..08f20339ec --- /dev/null +++ b/src/Avalonia.MicroCom/IMicroComExceptionCallback.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.MicroCom +{ + public interface IMicroComExceptionCallback + { + void RaiseException(Exception e); + } +} diff --git a/src/Avalonia.MicroCom/IMicroComShadowContainer.cs b/src/Avalonia.MicroCom/IMicroComShadowContainer.cs new file mode 100644 index 0000000000..a33d3a9811 --- /dev/null +++ b/src/Avalonia.MicroCom/IMicroComShadowContainer.cs @@ -0,0 +1,9 @@ +namespace Avalonia.MicroCom +{ + public interface IMicroComShadowContainer + { + MicroComShadow Shadow { get; set; } + void OnReferencedFromNative(); + void OnUnreferencedFromNative(); + } +} diff --git a/src/Avalonia.MicroCom/IUnknown.cs b/src/Avalonia.MicroCom/IUnknown.cs new file mode 100644 index 0000000000..a46953ced9 --- /dev/null +++ b/src/Avalonia.MicroCom/IUnknown.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.MicroCom +{ + public interface IUnknown : IDisposable + { + void AddRef(); + void Release(); + int QueryInterface(Guid guid, out IntPtr ppv); + T QueryInterface() where T : IUnknown; + } +} diff --git a/src/Avalonia.MicroCom/LocalInterop.cs b/src/Avalonia.MicroCom/LocalInterop.cs new file mode 100644 index 0000000000..785f4e03a5 --- /dev/null +++ b/src/Avalonia.MicroCom/LocalInterop.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.MicroCom +{ + unsafe class LocalInterop + { + public static unsafe void CalliStdCallvoid(void* thisObject, void* methodPtr) + { + throw null; + } + + public static unsafe int CalliStdCallint(void* thisObject, Guid* guid, IntPtr* ppv, void* methodPtr) + { + throw null; + } + } +} diff --git a/src/Avalonia.MicroCom/MicroComProxyBase.cs b/src/Avalonia.MicroCom/MicroComProxyBase.cs new file mode 100644 index 0000000000..56b80c2632 --- /dev/null +++ b/src/Avalonia.MicroCom/MicroComProxyBase.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.MicroCom +{ + public unsafe class MicroComProxyBase : IUnknown + { + private IntPtr _nativePointer; + private bool _ownsHandle; + + public IntPtr NativePointer + { + get + { + if (_nativePointer == IntPtr.Zero) + throw new ObjectDisposedException(this.GetType().FullName); + return _nativePointer; + } + } + + public void*** PPV => (void***)NativePointer; + + public MicroComProxyBase(IntPtr nativePointer, bool ownsHandle) + { + _nativePointer = nativePointer; + _ownsHandle = ownsHandle; + } + + protected virtual int VTableSize => 3; + + public void AddRef() + { + LocalInterop.CalliStdCallvoid(PPV, (*PPV)[1]); + } + + public void Release() + { + LocalInterop.CalliStdCallvoid(PPV, (*PPV)[2]); + } + + public int QueryInterface(Guid guid, out IntPtr ppv) + { + IntPtr r = default; + var rv = LocalInterop.CalliStdCallint(PPV, &guid, &r, (*PPV)[0]); + ppv = r; + return rv; + } + + public T QueryInterface() where T : IUnknown + { + var guid = MicroComRuntime.GetGuidFor(typeof(T)); + var rv = QueryInterface(guid, out var ppv); + if (rv != 0) + return (T)MicroComRuntime.CreateProxyFor(typeof(T), ppv, true); + throw new COMException("QueryInterface failed", rv); + } + + public bool IsDisposed => _nativePointer == IntPtr.Zero; + + public void Dispose() + { + if (_ownsHandle) + Release(); + _nativePointer = IntPtr.Zero; + } + + public bool OwnsHandle => _ownsHandle; + + public void EnsureOwned() + { + if (!_ownsHandle) + { + AddRef(); + _ownsHandle = true; + } + } + } +} diff --git a/src/Avalonia.MicroCom/MicroComRuntime.cs b/src/Avalonia.MicroCom/MicroComRuntime.cs new file mode 100644 index 0000000000..eccf24b496 --- /dev/null +++ b/src/Avalonia.MicroCom/MicroComRuntime.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; + +namespace Avalonia.MicroCom +{ + public static unsafe class MicroComRuntime + { + private static ConcurrentDictionary _vtables = new ConcurrentDictionary(); + + private static ConcurrentDictionary> _factories = + new ConcurrentDictionary>(); + private static ConcurrentDictionary _guids = new ConcurrentDictionary(); + private static ConcurrentDictionary _guidsToTypes = new ConcurrentDictionary(); + + static MicroComRuntime() + { + Register(typeof(IUnknown), new Guid("00000000-0000-0000-C000-000000000046"), + (ppv, owns) => new MicroComProxyBase(ppv, owns)); + RegisterVTable(typeof(IUnknown), MicroComVtblBase.Vtable); + } + + public static void RegisterVTable(Type t, IntPtr vtable) + { + _vtables[t] = vtable; + } + + public static void Register(Type t, Guid guid, Func proxyFactory) + { + _factories[t] = proxyFactory; + _guids[t] = guid; + _guidsToTypes[guid] = t; + } + + public static Guid GetGuidFor(Type type) => _guids[type]; + + public static T CreateProxyFor(void* ppv, bool ownsHandle) => (T)CreateProxyFor(typeof(T), new IntPtr(ppv), ownsHandle); + + public static object CreateProxyFor(Type type, IntPtr ppv, bool ownsHandle) => _factories[type](ppv, ownsHandle); + + public static void* GetNativePointer(T obj, bool owned = false) where T : IUnknown + { + if (obj is MicroComProxyBase proxy) + return (void*)proxy.NativePointer; + if (obj is IMicroComShadowContainer container) + { + container.Shadow ??= new MicroComShadow(container); + void* ptr = null; + var res = container.Shadow.GetOrCreateNativePointer(typeof(T), &ptr); + if (res != 0) + throw new COMException( + "Unable to create native callable wrapper for type " + typeof(T) + " for instance of type " + + obj.GetType(), + res); + if (owned) + container.Shadow.AddRef((Ccw*)ptr); + } + throw new ArgumentException("Unable to get a native pointer for " + obj); + } + + public static object GetObjectFromCcw(IntPtr ccw) + { + var ptr = (Ccw*)ccw; + var shadow = (MicroComShadow)GCHandle.FromIntPtr(ptr->GcShadowHandle).Target; + return shadow.Target; + } + + public static bool TryGetTypeForGuid(Guid guid, out Type t) => _guidsToTypes.TryGetValue(guid, out t); + + public static bool GetVtableFor(Type type, out IntPtr ptr) => _vtables.TryGetValue(type, out ptr); + + public static void UnhandledException(object target, Exception e) + { + if (target is IMicroComExceptionCallback cb) + { + try + { + cb.RaiseException(e); + } + catch + { + // We've tried + } + } + + } + } +} diff --git a/src/Avalonia.MicroCom/MicroComShadow.cs b/src/Avalonia.MicroCom/MicroComShadow.cs new file mode 100644 index 0000000000..c9411a089e --- /dev/null +++ b/src/Avalonia.MicroCom/MicroComShadow.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Avalonia.MicroCom +{ + public unsafe class MicroComShadow : IDisposable + { + private readonly object _lock = new object(); + private readonly Dictionary _shadows = new Dictionary(); + private readonly Dictionary _backShadows = new Dictionary(); + private GCHandle? _handle; + private volatile int _refCount; + internal IMicroComShadowContainer Target { get; } + internal MicroComShadow(IMicroComShadowContainer target) + { + Target = target; + Target.Shadow = this; + } + + internal int QueryInterface(Ccw* ccw, Guid* guid, void** ppv) + { + if (MicroComRuntime.TryGetTypeForGuid(*guid, out var type)) + return QueryInterface(type, ppv); + else + return unchecked((int)0x80004002u); + } + + internal int QueryInterface(Type type, void** ppv) + { + if (!type.IsInstanceOfType(Target)) + return unchecked((int)0x80004002u); + + var rv = GetOrCreateNativePointer(type, ppv); + if (rv == 0) + AddRef((Ccw*)*ppv); + return rv; + } + + internal int GetOrCreateNativePointer(Type type, void** ppv) + { + if (!MicroComRuntime.GetVtableFor(type, out var vtable)) + return unchecked((int)0x80004002u); + lock (_lock) + { + + if (_shadows.TryGetValue(type, out var shadow)) + { + var targetCcw = (Ccw*)shadow; + AddRef(targetCcw); + *ppv = targetCcw; + return 0; + } + else + { + var intPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + var targetCcw = (Ccw*)intPtr; + *targetCcw = default; + targetCcw->RefCount = 0; + targetCcw->VTable = vtable; + if (_handle == null) + _handle = GCHandle.Alloc(this); + targetCcw->GcShadowHandle = GCHandle.ToIntPtr(_handle.Value); + _shadows[type] = intPtr; + _backShadows[intPtr] = type; + *ppv = targetCcw; + + return 0; + } + } + } + + internal int AddRef(Ccw* ccw) + { + if (Interlocked.Increment(ref _refCount) == 1) + { + try + { + Target.OnUnreferencedFromNative(); + } + catch (Exception e) + { + MicroComRuntime.UnhandledException(Target, e); + } + } + + return Interlocked.Increment(ref ccw->RefCount); + } + + internal int Release(Ccw* ccw) + { + Interlocked.Decrement(ref _refCount); + var cnt = Interlocked.Decrement(ref ccw->RefCount); + if (cnt == 0) + return FreeCcw(ccw); + + return cnt; + } + + int FreeCcw(Ccw* ccw) + { + lock (_lock) + { + // Shadow got resurrected by a call to QueryInterface from another thread + if (ccw->RefCount != 0) + return ccw->RefCount; + + var intPtr = new IntPtr(ccw); + var type = _backShadows[intPtr]; + _backShadows.Remove(intPtr); + _shadows.Remove(type); + Marshal.FreeHGlobal(intPtr); + if (_shadows.Count == 0) + { + _handle?.Free(); + _handle = null; + try + { + Target.OnUnreferencedFromNative(); + } + catch(Exception e) + { + MicroComRuntime.UnhandledException(Target, e); + } + } + } + + return 0; + } + + /* + Needs to be called to support the following scenario: + 1) Object created + 2) Object passed to native code, shadow is created, CCW is created + 3) Native side has never called AddRef + + In that case the GC handle to the shadow object is still alive + */ + + public void Dispose() + { + lock (_lock) + { + List toRemove = null; + foreach (var kv in _backShadows) + { + var ccw = (Ccw*)kv.Key; + if (ccw->RefCount == 0) + { + toRemove ??= new List(); + toRemove.Add(kv.Key); + } + } + + if(toRemove != null) + foreach (var intPtr in toRemove) + FreeCcw((Ccw*)intPtr); + } + } + } + + [StructLayout(LayoutKind.Sequential)] + struct Ccw + { + public IntPtr VTable; + public IntPtr GcShadowHandle; + public volatile int RefCount; + public MicroComShadow GetShadow() => (MicroComShadow)GCHandle.FromIntPtr(GcShadowHandle).Target; + } +} diff --git a/src/Avalonia.MicroCom/MicroComVtblBase.cs b/src/Avalonia.MicroCom/MicroComVtblBase.cs new file mode 100644 index 0000000000..4cd419cdaf --- /dev/null +++ b/src/Avalonia.MicroCom/MicroComVtblBase.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Avalonia.MicroCom +{ + public unsafe class MicroComVtblBase + { + private List _methods = new List(); + [UnmanagedFunctionPointerAttribute(CallingConvention.ThisCall)] + private delegate int AddRefDelegate(Ccw* ccw); + + [UnmanagedFunctionPointerAttribute(CallingConvention.ThisCall)] + private delegate int QueryInterfaceDelegate(Ccw* ccw, Guid* guid, void** ppv); + + public static IntPtr Vtable { get; } = new MicroComVtblBase().CreateVTable(); + public MicroComVtblBase() + { + AddMethod((QueryInterfaceDelegate)QueryInterface); + AddMethod((AddRefDelegate)AddRef); + } + + protected void AddMethod(Delegate d) + { + GCHandle.Alloc(d); + _methods.Add(Marshal.GetFunctionPointerForDelegate(d)); + } + + protected unsafe IntPtr CreateVTable() + { + var ptr = (IntPtr*)Marshal.AllocHGlobal((IntPtr.Size + 1) * _methods.Count); + for (var c = 0; c < _methods.Count; c++) + ptr[c] = _methods[c]; + return new IntPtr(ptr); + } + + static int QueryInterface(Ccw* ccw, Guid* guid, void** ppv) => ccw->GetShadow().QueryInterface(ccw, guid, ppv); + static int AddRef(Ccw* ccw) => ccw->GetShadow().AddRef(ccw); + static int Release(Ccw* ccw) => ccw->GetShadow().Release(ccw); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl new file mode 100644 index 0000000000..c763007826 --- /dev/null +++ b/src/Avalonia.Native/avn.idl @@ -0,0 +1,522 @@ +@clr-namespace Avalonia.Native.MicroCom +@clr-access internal +@cpp-preamble @@ +#include "com.h" +#include "key.h" +#include "stddef.h" +@@ + +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +} + +struct AvnSize +{ + double Width, Height; +} + +struct AvnPixelSize +{ + int Width, Height; +} + +struct AvnRect +{ + double X, Y, Width, Height; +} + +struct AvnVector +{ + double X, Y; +} + +struct AvnPoint +{ + double X, Y; +} + +struct AvnScreen +{ + AvnRect Bounds; + AvnRect WorkingArea; + float PixelDensity; + bool Primary; +} + +enum AvnPixelFormat +{ + kAvnRgb565, + kAvnRgba8888, + kAvnBgra8888 +} + +struct AvnFramebuffer +{ + void* Data; + int Width; + int Height; + int Stride; + AvnVector Dpi; + AvnPixelFormat PixelFormat; +} + +struct AvnColor +{ + byte Alpha; + byte Red; + byte Green; + byte Blue; +} + +enum AvnRawMouseEventType +{ + LeaveWindow, + LeftButtonDown, + LeftButtonUp, + RightButtonDown, + RightButtonUp, + MiddleButtonDown, + MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, + Move, + Wheel, + NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd, + TouchCancel +} + +enum AvnRawKeyEventType +{ + KeyDown, + KeyUp +} + +enum AvnInputModifiers +{ + AvnInputModifiersNone = 0, + Alt = 1, + Control = 2, + Shift = 4, + Windows = 8, + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256 +} + +[class-enum] +enum AvnDragDropEffects +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, +} + +[class-enum] +enum AvnDragEventType +{ + Enter, + Over, + Leave, + Drop +} + +enum AvnWindowState +{ + Normal, + Minimized, + Maximized, + FullScreen, +} + +enum AvnStandardCursorType +{ + CursorArrow, + CursorIbeam, + CursorWait, + CursorCross, + CursorUpArrow, + CursorSizeWestEast, + CursorSizeNorthSouth, + CursorSizeAll, + CursorNo, + CursorHand, + CursorAppStarting, + CursorHelp, + CursorTopSide, + CursorBottomSize, + CursorLeftSide, + CursorRightSide, + CursorTopLeftCorner, + CursorTopRightCorner, + CursorBottomLeftCorner, + CursorBottomRightCorner, + CursorDragMove, + CursorDragCopy, + CursorDragLink, + CursorNone +} + +enum AvnWindowEdge +{ + WindowEdgeNorthWest, + WindowEdgeNorth, + WindowEdgeNorthEast, + WindowEdgeWest, + WindowEdgeEast, + WindowEdgeSouthWest, + WindowEdgeSouth, + WindowEdgeSouthEast +} + +enum AvnMenuItemToggleType +{ + None, + CheckMark, + Radio +} + +enum AvnExtendClientAreaChromeHints +{ + AvnNoChrome = 0, + AvnSystemChrome = 0x01, + AvnPreferSystemChrome = 0x02, + AvnOSXThickTitleBar = 0x08, + AvnDefaultChrome = AvnSystemChrome, +} + +[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] +interface IAvaloniaNativeFactory : IUnknown +{ + HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator); + IAvnMacOptions* GetMacOptions(); + HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv); + HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv); + HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv); + HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv); + HRESULT CreateScreens(IAvnScreens** ppv); + HRESULT CreateClipboard(IAvnClipboard** ppv); + HRESULT CreateDndClipboard(IAvnClipboard** ppv); + HRESULT CreateCursorFactory(IAvnCursorFactory** ppv); + HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv); + HRESULT SetAppMenu(IAvnMenu* menu); + HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); + HRESULT CreateMenuItem(IAvnMenuItem** ppv); + HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv); +} + +[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] +interface IAvnString : IUnknown +{ + HRESULT Pointer(void**retOut); + HRESULT Length(int*ret); +} + +[uuid(e5aca675-02b7-4129-aa79-d6e417210bda)] +interface IAvnWindowBase : IUnknown +{ + HRESULT Show(); + HRESULT Hide(); + HRESULT Close(); + HRESULT Activate(); + HRESULT GetClientSize(AvnSize*ret); + HRESULT GetScaling(double*ret); + HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize); + HRESULT Resize(double width, double height); + HRESULT Invalidate(AvnRect rect); + HRESULT BeginMoveDrag(); + HRESULT BeginResizeDrag(AvnWindowEdge edge); + HRESULT GetPosition(AvnPoint*ret); + HRESULT SetPosition(AvnPoint point); + HRESULT PointToClient(AvnPoint point, AvnPoint*ret); + HRESULT PointToScreen(AvnPoint point, AvnPoint*ret); + HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose); + HRESULT SetTopMost(bool value); + HRESULT SetCursor(IAvnCursor* cursor); + HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret); + HRESULT SetMainMenu(IAvnMenu* menu); + HRESULT ObtainNSWindowHandle(void** retOut); + HRESULT ObtainNSWindowHandleRetained(void** retOut); + HRESULT ObtainNSViewHandle(void** retOut); + HRESULT ObtainNSViewHandleRetained(void** retOut); + HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut); + HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle); + HRESULT SetBlurEnabled(bool enable); +} + +[uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91)] +interface IAvnPopup : IAvnWindowBase +{ + +} + +[uuid(cab661de-49d6-4ead-b59c-eac9b2b6c28d)] +interface IAvnWindow : IAvnWindowBase +{ + HRESULT SetEnabled(bool enable); + HRESULT SetParent(IAvnWindow* parent); + HRESULT SetCanResize(bool value); + HRESULT SetDecorations(SystemDecorations value); + HRESULT SetTitle(void* utf8Title); + HRESULT SetTitleBarColor(AvnColor color); + HRESULT SetWindowState(AvnWindowState state); + HRESULT GetWindowState(AvnWindowState*ret); + HRESULT TakeFocusFromChildren(); + HRESULT SetExtendClientArea(bool enable); + HRESULT SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints); + HRESULT GetExtendTitleBarHeight(double*ret); + HRESULT SetExtendTitleBarHeight(double value); +} + +[uuid(939b6599-40a8-4710-a4c8-5d72d8f174fb)] +interface IAvnWindowBaseEvents : IUnknown +{ + HRESULT Paint(); + void Closed(); + void Activated(); + void Deactivated(); + void Resized([const] AvnSize* size); + void PositionChanged(AvnPoint position); + void RawMouseEvent(AvnRawMouseEventType type, + uint timeStamp, + AvnInputModifiers modifiers, + AvnPoint point, + AvnVector delta); + bool RawKeyEvent(AvnRawKeyEventType type, uint timeStamp, AvnInputModifiers modifiers, uint key); + bool RawTextInputEvent(uint timeStamp, [const] char* text); + void ScalingChanged(double scaling); + void RunRenderPriorityJobs(); + void LostFocus(); + AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, AvnDragDropEffects effects, + IAvnClipboard* clipboard, void* dataObjectHandle); +} + +[uuid(1ae178ee-1fcc-447f-b6dd-b7bb727f934c)] +interface IAvnWindowEvents : IAvnWindowBaseEvents +{ + /** + * Closing Event + * Called when the user presses the OS window close button. + * return true to allow the close, return false to prevent close. + */ + bool Closing(); + + void WindowStateChanged(AvnWindowState state); + + void GotInputWhenDisabled(); +} + +[uuid(e34ae0f8-18b4-48a3-b09d-2e6b19a3cf5e)] +interface IAvnMacOptions : IUnknown +{ + HRESULT SetShowInDock(int show); + HRESULT SetApplicationTitle(void* utf8string); +} + +[uuid(04c1b049-1f43-418a-9159-cae627ec1367)] +interface IAvnActionCallback : IUnknown +{ + void Run(); +} + +[uuid(6df4d2db-0b80-4f59-ad88-0baa5e21eb14)] +interface IAvnSignaledCallback : IUnknown +{ + void Signaled(int priority, bool priorityContainsMeaningfulValue); +} + +[uuid(97330f88-c22b-4a8e-a130-201520091b01)] +interface IAvnLoopCancellation : IUnknown +{ + void Cancel(); +} + +[uuid(fbc06f3d-7860-42df-83fd-53c4b02dd9c3)] +interface IAvnPlatformThreadingInterface : IUnknown +{ + bool GetCurrentThreadIsLoopThread(); + void SetSignaledCallback(IAvnSignaledCallback* cb); + IAvnLoopCancellation* CreateLoopCancellation(); + HRESULT RunLoop(IAvnLoopCancellation* cancel); + // Can't pass int* to sharpgentools for some reason + void Signal(int priority); + IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback); +} + +[uuid(6c621a6e-e4c1-4ae3-9749-83eeeffa09b6)] +interface IAvnSystemDialogEvents : IUnknown +{ + void OnCompleted(int numResults, void* ptrFirstResult); +} + +[uuid(4d7a47db-a944-4061-abe7-62cb6aa0ffd5)] +interface IAvnSystemDialogs : IUnknown +{ + void SelectFolderDialog(IAvnWindow* parentWindowHandle, + IAvnSystemDialogEvents* events, + [const] char* title, + [const] char* initialPath); + + void OpenFileDialog(IAvnWindow* parentWindowHandle, + IAvnSystemDialogEvents* events, + bool allowMultiple, + [const] char* title, + [const] char* initialDirectory, + [const] char* initialFile, + [const] char* filters); + + void SaveFileDialog(IAvnWindow* parentWindowHandle, + IAvnSystemDialogEvents* events, + [const] char* title, + [const] char* initialDirectory, + [const] char* initialFile, + [const] char* filters); +} + +[uuid(9a52bc7a-d8c7-4230-8d34-704a0b70a933)] +interface IAvnScreens : IUnknown +{ + HRESULT GetScreenCount(int* ret); + HRESULT GetScreen(int index, AvnScreen* ret); +} + +[uuid(792b1bd4-76cc-46ea-bfd0-9d642154b1b3)] +interface IAvnClipboard : IUnknown +{ + HRESULT GetText(char* type, IAvnString**ppv); + HRESULT SetText(char* type, void* utf8Text); + HRESULT ObtainFormats(IAvnStringArray**ppv); + HRESULT GetStrings(char* type, IAvnStringArray**ppv); + HRESULT SetBytes(char* type, void* utf8Text, int len); + HRESULT GetBytes(char* type, IAvnString**ppv); + + HRESULT Clear(); +} + +[uuid(3f998545-f027-4d4d-bd2a-1a80926d984e)] +interface IAvnCursor : IUnknown +{ +} + +[uuid(51ecfb12-c427-4757-a2c9-1596bfce53ef)] +interface IAvnCursorFactory : IUnknown +{ + HRESULT GetCursor(AvnStandardCursorType cursorType, IAvnCursor** retOut); +} + +[uuid(60452465-8616-40af-bc00-042e69828ce7)] +interface IAvnGlDisplay : IUnknown +{ + HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv); + void LegacyClearCurrentContext(); + HRESULT WrapContext(void* native, IAvnGlContext**ppv); + void* GetProcAddress(char* proc); +} + +[uuid(78c5711e-2a98-40d2-bac4-0cc9a49dc4f3)] +interface IAvnGlContext : IUnknown +{ + HRESULT MakeCurrent(IUnknown** ppv); + HRESULT LegacyMakeCurrent(); + int GetSampleCount(); + int GetStencilSize(); + void* GetNativeHandle(); +} + +[uuid(931062d2-5bc8-4062-8588-83dd8deb99c2)] +interface IAvnGlSurfaceRenderTarget : IUnknown +{ + HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret); +} + +[uuid(e625b406-f04c-484e-946a-4abd2c6015ad)] +interface IAvnGlSurfaceRenderingSession : IUnknown +{ + HRESULT GetPixelSize(AvnPixelSize* ret); + HRESULT GetScaling(double* ret); +} + +[uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] +interface IAvnMenu : IUnknown +{ + HRESULT InsertItem(int index, IAvnMenuItem* item); + HRESULT RemoveItem(IAvnMenuItem* item); + HRESULT SetTitle(void* utf8String); + HRESULT Clear(); +} + +[uuid(59e0586d-bd1c-4b85-9882-80d448b0fed9)] +interface IAvnPredicateCallback : IUnknown +{ + bool Evaluate(); +} + +[uuid(f890219a-1720-4cd5-9a26-cd95fccbf53c)] +interface IAvnMenuItem : IUnknown +{ + HRESULT SetSubMenu(IAvnMenu* menu); + HRESULT SetTitle(void* utf8String); + HRESULT SetGesture(void* utf8String, AvnInputModifiers modifiers); + HRESULT SetAction(IAvnPredicateCallback* predicate, IAvnActionCallback* callback); + HRESULT SetIsChecked(bool isChecked); + HRESULT SetToggleType(AvnMenuItemToggleType toggleType); + HRESULT SetIcon(void* data, size_t length); +} + +[uuid(0af7df53-7632-42f4-a650-0992c361b477)] +interface IAvnMenuEvents : IUnknown +{ + /** + * NeedsUpdate + */ + void NeedsUpdate(); +} + +[uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)] +interface IAvnStringArray : IUnknown +{ + uint GetCount(); + HRESULT Get(uint index, IAvnString**ppv); +} + +[uuid(a13d2382-3b3a-4d1c-9b27-8f34653d3f01)] +interface IAvnDndResultCallback : IUnknown +{ + void OnDragAndDropComplete(AvnDragDropEffects effecct); +} + +[uuid(f07c608e-52e9-422d-836e-c70f6e9b80f5)] +interface IAvnGCHandleDeallocatorCallback : IUnknown +{ + void FreeGCHandle(void* handle); +} + +[uuid(91c7f677-f26b-4ff3-93cc-cf15aa966ffa)] +interface IAvnNativeControlHost : IUnknown +{ + HRESULT CreateDefaultChild(void* parent, void** retOut); + IAvnNativeControlHostTopLevelAttachment* CreateAttachment(); + void DestroyDefaultChild(void* child); +} + +[uuid(14a9e164-1aae-4271-bb78-7b5230999b52)] +interface IAvnNativeControlHostTopLevelAttachment : IUnknown +{ + void* GetParentHandle(); + HRESULT InitializeWithChildHandle(void* child); + HRESULT AttachTo(IAvnNativeControlHost* host); + void ShowInBounds(float x, float y, float width, float height); + void HideWithSize(float width, float height); + void ReleaseChild(); +} diff --git a/src/tools/MicroComGenerator/Ast.cs b/src/tools/MicroComGenerator/Ast.cs new file mode 100644 index 0000000000..8613cbba88 --- /dev/null +++ b/src/tools/MicroComGenerator/Ast.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; + +namespace MicroComGenerator.Ast +{ + public class AstAttributeNode + { + public string Name { get; set; } + public string Value { get; set; } + + public AstAttributeNode(string name, string value) + { + Name = name; + Value = value; + } + + public override string ToString() => $"{Name} = {Value}"; + } + + public class AstEnumNode : List + { + public List Attributes { get; set; } = new List(); + public string Name { get; set; } + public override string ToString() => Name; + } + + public class AstEnumMemberNode + { + public string Name { get; set; } + public string Value { get; set; } + + public AstEnumMemberNode(string name, string value) + { + Name = name; + Value = value; + } + + public override string ToString() => $"{Name} = {Value}"; + } + + public class AstStructNode : List + { + public List Attributes { get; set; } = new List(); + public string Name { get; set; } + public override string ToString() => Name; + } + + public class AstTypeNode + { + public string Name { get; set; } + public int PointerLevel { get; set; } + + public override string ToString() => Name + new string('*', PointerLevel); + } + + public class AstStructMemberNode + { + public string Name { get; set; } + public AstTypeNode Type { get; set; } + + public override string ToString() => $"{Type} {Name}"; + } + + public class AstInterfaceNode : List + { + public List Attributes { get; set; } = new List(); + public string Name { get; set; } + public string Inherits { get; set; } + + public override string ToString() + { + if (Inherits == null) + return Name; + return $"{Name} : {Inherits}"; + } + } + + public class AstInterfaceMemberNode : List + { + public string Name { get; set; } + public AstTypeNode ReturnType { get; set; } + public List Attributes { get; set; } = new List(); + + public override string ToString() => $"{ReturnType} {Name} ({string.Join(", ", this)})"; + } + + public class AstInterfaceMemberArgumentNode + { + public string Name { get; set; } + public AstTypeNode Type { get; set; } + public List Attributes { get; set; } = new List(); + + public override string ToString() => $"{Type} {Name}"; + } + + public class AstIdlNode + { + public List Attributes { get; set; } = new List(); + public List Enums { get; set; } = new List(); + public List Structs { get; set; } = new List(); + public List Interfaces { get; set; } = new List(); + } +} diff --git a/src/tools/MicroComGenerator/AstParser.cs b/src/tools/MicroComGenerator/AstParser.cs new file mode 100644 index 0000000000..46404da3d7 --- /dev/null +++ b/src/tools/MicroComGenerator/AstParser.cs @@ -0,0 +1,228 @@ +using System.Collections.Generic; +using MicroComGenerator.Ast; + +namespace MicroComGenerator +{ + public class AstParser + { + public static AstIdlNode Parse(string source) + { + var parser = new TokenParser(source); + var idl = new AstIdlNode { Attributes = ParseGlobalAttributes(ref parser) }; + + while (!parser.Eof) + { + var attrs = ParseLocalAttributes(ref parser); + + if (parser.TryParseKeyword("enum")) + idl.Enums.Add(ParseEnum(attrs, ref parser)); + else if (parser.TryParseKeyword("struct")) + idl.Structs.Add(ParseStruct(attrs, ref parser)); + else if (parser.TryParseKeyword("interface")) + idl.Interfaces.Add(ParseInterface(attrs, ref parser)); + else + throw new ParseException("Unexpected character", ref parser); + } + + return idl; + } + + static List ParseGlobalAttributes(ref TokenParser parser) + { + var rv = new List(); + while (!parser.Eof) + { + parser.SkipWhitespace(); + if (parser.TryConsume('@')) + { + var ident = parser.ParseIdentifier("-"); + var value = parser.ReadToEol().Trim(); + if (value == "@@") + { + parser.Advance(1); + value = ""; + while (true) + { + var l = parser.ReadToEol(); + if (l == "@@") + break; + else + value = value.Length == 0 ? l : (value + "\n" + l); + parser.Advance(1); + } + + } + rv.Add(new AstAttributeNode(ident, value)); + } + else + return rv; + } + + return rv; + } + + static List ParseLocalAttributes(ref TokenParser parser) + { + var rv = new List(); + if (parser.TryConsume("[")) + { + while (!parser.TryConsume("]") && !parser.Eof) + { + if (parser.TryConsume(',')) + continue; + + // Get identifier + var ident = parser.ParseIdentifier("-"); + + // No value, end of attribute list + if (parser.TryConsume(']')) + { + rv.Add(new AstAttributeNode(ident, null)); + return rv; + } + // No value, next attribute + else if (parser.TryConsume(',')) + rv.Add(new AstAttributeNode(ident, null)); + // Has value + else if (parser.TryConsume('(')) + { + var value = parser.ReadTo(')'); + parser.Consume(')'); + rv.Add(new AstAttributeNode(ident, value)); + } + else + throw new ParseException("Unexpected character", ref parser); + } + + if (parser.Eof) + throw new ParseException("Unexpected EOF", ref parser); + } + + return rv; + } + + static void EnsureOpenBracket(ref TokenParser parser) + { + if (!parser.TryConsume('{')) + throw new ParseException("{ expected", ref parser); + } + + static AstEnumNode ParseEnum(List attrs, ref TokenParser parser) + { + var name = parser.ParseIdentifier(); + EnsureOpenBracket(ref parser); + var rv = new AstEnumNode { Name = name, Attributes = attrs }; + while (!parser.TryConsume('}') && !parser.Eof) + { + if (parser.TryConsume(',')) + continue; + + var ident = parser.ParseIdentifier(); + + // Automatic value + if (parser.TryConsume(',') || parser.Peek == '}') + { + rv.Add(new AstEnumMemberNode(ident, null)); + continue; + } + + if (!parser.TryConsume('=')) + throw new ParseException("Unexpected character", ref parser); + + var value = parser.ReadToAny(",}").Trim(); + rv.Add(new AstEnumMemberNode(ident, value)); + + if (parser.Eof) + throw new ParseException("Unexpected EOF", ref parser); + } + + + return rv; + } + + static AstTypeNode ParseType(ref TokenParser parser) + { + var ident = parser.ParseIdentifier(); + var t = new AstTypeNode { Name = ident }; + while (parser.TryConsume('*')) + t.PointerLevel++; + return t; + } + + static AstStructNode ParseStruct(List attrs, ref TokenParser parser) + { + var name = parser.ParseIdentifier(); + EnsureOpenBracket(ref parser); + var rv = new AstStructNode { Name = name, Attributes = attrs }; + while (!parser.TryConsume('}') && !parser.Eof) + { + var t = ParseType(ref parser); + bool parsedAtLeastOneMember = false; + while (!parser.TryConsume(';')) + { + // Skip any , + while (parser.TryConsume(',')) { } + + var ident = parser.ParseIdentifier(); + parsedAtLeastOneMember = true; + rv.Add(new AstStructMemberNode { Name = ident, Type = t }); + } + + if (!parsedAtLeastOneMember) + throw new ParseException("Expected at least one enum member with declared type " + t, ref parser); + } + + return rv; + } + + static AstInterfaceNode ParseInterface(List interfaceAttrs, ref TokenParser parser) + { + var interfaceName = parser.ParseIdentifier(); + string inheritsFrom = null; + if (parser.TryConsume(":")) + inheritsFrom = parser.ParseIdentifier(); + + EnsureOpenBracket(ref parser); + var rv = new AstInterfaceNode + { + Name = interfaceName, Attributes = interfaceAttrs, Inherits = inheritsFrom + }; + while (!parser.TryConsume('}') && !parser.Eof) + { + var memberAttrs = ParseLocalAttributes(ref parser); + var returnType = ParseType(ref parser); + var name = parser.ParseIdentifier(); + var member = new AstInterfaceMemberNode + { + Name = name, ReturnType = returnType, Attributes = memberAttrs + }; + rv.Add(member); + + parser.Consume('('); + while (true) + { + if (parser.TryConsume(')')) + break; + + var argumentAttrs = ParseLocalAttributes(ref parser); + var type = ParseType(ref parser); + var argName = parser.ParseIdentifier(); + member.Add(new AstInterfaceMemberArgumentNode + { + Name = argName, Type = type, Attributes = argumentAttrs + }); + + if (parser.TryConsume(')')) + break; + if (parser.TryConsume(',')) + continue; + throw new ParseException("Unexpected character", ref parser); + } + + parser.Consume(';'); + } + + return rv; + } + } +} diff --git a/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs b/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs new file mode 100644 index 0000000000..2fcf1b404b --- /dev/null +++ b/src/tools/MicroComGenerator/CSharpGen.InterfaceGen.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using MicroComGenerator.Ast; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +// ReSharper disable CoVariantArrayConversion + +// HERE BE DRAGONS + +namespace MicroComGenerator +{ + public partial class CSharpGen + { + abstract class Arg + { + public string Name; + public string NativeType; + + public virtual StatementSyntax CreateFixed(StatementSyntax inner) => inner; + + public virtual void PreMarshal(List body) + { + } + + public virtual void PreMarshalForReturn(List body) => + throw new InvalidOperationException("Don't know how to use " + NativeType + " as HRESULT-return"); + + public virtual ExpressionSyntax Value(bool isHresultReturn) => ParseExpression(Name); + public abstract string ManagedType { get; } + public virtual string ReturnManagedType => ManagedType; + + public virtual StatementSyntax[] ReturnMarshalResult() => new[] { ParseStatement("return " + Name + ";") }; + + + public virtual void BackPreMarshal(List body) + { + } + + public virtual ExpressionSyntax BackMarshalValue() => ParseExpression(Name); + public virtual ExpressionSyntax BackMarshalReturn(string resultVar) => ParseExpression(resultVar); + + } + + class InterfaceReturnArg : Arg + { + public string InterfaceType; + public override ExpressionSyntax Value(bool isHresultReturn) => ParseExpression("&" + PName); + public override string ManagedType => InterfaceType; + + private string PName => "__marshal_" + Name; + + public override void PreMarshalForReturn(List body) + { + body.Add(ParseStatement("void* " + PName + " = null;")); + } + + public override StatementSyntax[] ReturnMarshalResult() => new[] + { + ParseStatement("return Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + + PName + ", true);") + }; + + public override ExpressionSyntax BackMarshalValue() + { + return ParseExpression("INVALID"); + } + + public override ExpressionSyntax BackMarshalReturn(string resultVar) + { + return ParseExpression($"Avalonia.MicroCom.MicroComRuntime.GetNativePointer({resultVar}, true)"); + } + } + + class InterfaceArg : Arg + { + public string InterfaceType; + + public override ExpressionSyntax Value(bool isHresultReturn) => + ParseExpression("Avalonia.MicroCom.MicroComRuntime.GetNativePointer(" + Name + ")"); + + public override string ManagedType => InterfaceType; + + public override StatementSyntax[] ReturnMarshalResult() => new[] + { + ParseStatement("return Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + + Name + ", true);") + }; + + public override ExpressionSyntax BackMarshalValue() + { + return ParseExpression("Avalonia.MicroCom.MicroComRuntime.CreateProxyFor<" + InterfaceType + ">(" + + Name + ", false)"); + } + + public override ExpressionSyntax BackMarshalReturn(string resultVar) + { + return ParseExpression($"Avalonia.MicroCom.MicroComRuntime.GetNativePointer({resultVar}, true)"); + } + } + + class BypassArg : Arg + { + public string Type { get; set; } + public int PointerLevel; + public override string ManagedType => Type + new string('*', PointerLevel); + public override string ReturnManagedType => Type + new string('*', PointerLevel - 1); + + public override ExpressionSyntax Value(bool isHresultReturn) + { + if (isHresultReturn) + return ParseExpression("&" + Name); + return base.Value(false); + } + + public override void PreMarshalForReturn(List body) + { + if (PointerLevel == 0) + base.PreMarshalForReturn(body); + else + body.Add(ParseStatement(Type + new string('*', PointerLevel - 1) + " " + Name + "=default;")); + } + } + + class StringArg : Arg + { + private string BName => "__bytemarshal_" + Name; + private string FName => "__fixedmarshal_" + Name; + + public override void PreMarshal(List body) + { + body.Add(ParseStatement($"var {BName} = new byte[System.Text.Encoding.UTF8.GetByteCount({Name})+1];")); + body.Add(ParseStatement($"System.Text.Encoding.UTF8.GetBytes({Name}, 0, {Name.Length}, {BName}, 0);")); + } + + public override StatementSyntax CreateFixed(StatementSyntax inner) + { + return FixedStatement(DeclareVar("byte*", FName, ParseExpression(BName)), inner); + } + + public override ExpressionSyntax Value(bool isHresultReturn) => ParseExpression(FName); + public override string ManagedType => "string"; + public override ExpressionSyntax BackMarshalValue() + { + return ParseExpression( + $"({Name} == null ? null : System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new IntPtr(" + Name + ")))"); + } + } + + string ConvertNativeType(string type) + { + if (type == "size_t") + return "System.IntPtr"; + if (type == "HRESULT") + return "int"; + return type; + } + + Arg ConvertArg(string name, AstTypeNode type) + { + type = new AstTypeNode { Name = ConvertNativeType(type.Name), PointerLevel = type.PointerLevel }; + + if (type.PointerLevel == 2) + { + if (type.Name.StartsWith("I")) + return new InterfaceReturnArg { Name = name, InterfaceType = type.Name, NativeType = "void**" }; + } + else if (type.PointerLevel == 1) + { + if (type.Name.StartsWith("I")) + return new InterfaceArg { Name = name, InterfaceType = type.Name, NativeType = "void*" }; + if (type.Name == "char") + return new StringArg { Name = name, NativeType = "byte*" }; + } + + return new BypassArg + { + Name = name, Type = type.Name, PointerLevel = type.PointerLevel, NativeType = type.ToString() + }; + } + + + void GenerateInterfaceMember(AstInterfaceMemberNode member, ref InterfaceDeclarationSyntax iface, + ref ClassDeclarationSyntax proxy, ref ClassDeclarationSyntax vtbl, + List vtblCtor, int num) + { + // Prepare method information + var args = member.Select(a => ConvertArg(a.Name, a.Type)).ToList(); + var returnArg = ConvertArg("__result", member.ReturnType); + bool isHresult = member.ReturnType.Name == "HRESULT"; + bool isHresultLastArgumentReturn = isHresult + && args.Count > 0 + && (args.Last().Name == "ppv" || args.Last().Name == "retOut" || args.Last().Name == "ret") + && ((member.Last().Type.PointerLevel > 0 + && !member.Last().Type.Name + .StartsWith("I")) + || member.Last().Type.PointerLevel == 2); + + bool isVoidReturn = member.ReturnType.Name == "void" && member.ReturnType.PointerLevel == 0; + + + // Generate method signature + MethodDeclarationSyntax GenerateManagedSig(string returnType, string name, + IEnumerable<(string n, string t)> args) + => MethodDeclaration(ParseTypeName(returnType), name).WithParameterList( + ParameterList( + SeparatedList(args.Select(x => Parameter(Identifier(x.n)).WithType(ParseTypeName(x.t)))))); + + var managedSig = + isHresult ? + GenerateManagedSig(isHresultLastArgumentReturn ? args.Last().ReturnManagedType : "void", + member.Name, + (isHresultLastArgumentReturn ? args.SkipLast(1) : args).Select(a => (a.Name, a.ManagedType))) : + GenerateManagedSig(returnArg.ManagedType, member.Name, args.Select(a => (a.Name, a.ManagedType))); + + iface = iface.AddMembers(managedSig.WithSemicolonToken(Semicolon())); + + // Prepare args for marshaling + var preMarshal = new List(); + if (!isVoidReturn) + preMarshal.Add(ParseStatement(returnArg.NativeType + " __result;")); + + for (var idx = 0; idx < args.Count; idx++) + { + if (isHresultLastArgumentReturn && idx == args.Count - 1) + args[idx].PreMarshalForReturn(preMarshal); + else + args[idx].PreMarshal(preMarshal); + } + + // Generate call expression + ExpressionSyntax callExpr = InvocationExpression(_localInterop.GetCaller(returnArg.NativeType, + args.Select(x => x.NativeType).ToList())) + .AddArgumentListArguments(Argument(ParseExpression("PPV"))) + .AddArgumentListArguments(args + .Select((a, i) => Argument(a.Value(isHresultLastArgumentReturn && i == args.Count - 1))).ToArray()) + .AddArgumentListArguments(Argument(ParseExpression("PPV[base.VTableSize + " + num + "]"))); + + // Save call result if needed + if (!isVoidReturn) + callExpr = AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, ParseExpression("__result"), + callExpr); + + + // Wrap call into fixed() blocks + StatementSyntax callStatement = ExpressionStatement(callExpr); + foreach (var arg in args) + callStatement = arg.CreateFixed(callStatement); + + // Build proxy body + var proxyBody = Block() + .AddStatements(preMarshal.ToArray()) + .AddStatements(callStatement); + + // Process return value + if (!isVoidReturn) + { + if (isHresult) + { + proxyBody = proxyBody.AddStatements( + ParseStatement( + $"if(__result != 0) throw new System.Runtime.InteropServices.COMException(\"{member.Name} failed\", __result);")); + + if (isHresultLastArgumentReturn) + proxyBody = proxyBody.AddStatements(args.Last().ReturnMarshalResult()); + } + else + proxyBody = proxyBody.AddStatements(returnArg.ReturnMarshalResult()); + } + + // Add the proxy method + proxy = proxy.AddMembers(managedSig.AddModifiers(SyntaxKind.PublicKeyword) + .WithBody(proxyBody)); + + + // Generate VTable method + + var shadowDelegate = DelegateDeclaration(ParseTypeName(returnArg.NativeType), member.Name+"Delegate") + .AddParameterListParameters(Parameter(Identifier("@this")).WithType(ParseTypeName("IntPtr"))) + .AddParameterListParameters(args.Select(x => + Parameter(Identifier(x.Name)).WithType(ParseTypeName(x.NativeType))).ToArray()); + + var shadowMethod = MethodDeclaration(shadowDelegate.ReturnType, member.Name) + .WithParameterList(shadowDelegate.ParameterList) + .AddModifiers(Token(SyntaxKind.StaticKeyword)); + + var backPreMarshal = new List(); + foreach (var arg in args) + arg.BackPreMarshal(backPreMarshal); + + backPreMarshal.Add( + ParseStatement($"__target = ({iface.Identifier.Text})Avalonia.MicroCom.MicroComRuntime.GetObjectFromCcw(@this);")); + + var isBackVoidReturn = isVoidReturn || (isHresult && !isHresultLastArgumentReturn); + + StatementSyntax backCallStatement; + + var backCallExpr = + IsPropertyRewriteCandidate(managedSig) ? + ParseExpression("__target." + member.Name.Substring(3)) : + InvocationExpression(ParseExpression("__target." + member.Name)) + .WithArgumentList(ArgumentList(SeparatedList( + (isHresultLastArgumentReturn ? args.SkipLast(1) : args) + .Select(a => + Argument(a.BackMarshalValue()))))); + + if (isBackVoidReturn) + backCallStatement = ExpressionStatement(backCallExpr); + else + { + backCallStatement = LocalDeclarationStatement(DeclareVar("var", "__result", backCallExpr)); + if (isHresultLastArgumentReturn) + { + backCallStatement = Block(backCallStatement, + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + ParseExpression("*" + args.Last().Name), + args.Last().BackMarshalReturn("__result") + ))); + + } + else + backCallStatement = Block(backCallStatement, + ReturnStatement(returnArg.BackMarshalReturn("__result"))); + } + + BlockSyntax backBodyBlock = Block().AddStatements(backPreMarshal.ToArray()).AddStatements(backCallStatement); + + + backBodyBlock = Block( + TryStatement( + SingletonList(CatchClause( + CatchDeclaration(ParseTypeName("System.Exception"), Identifier("__exception__")), null, + Block( + ParseStatement( + "Avalonia.MicroCom.MicroComRuntime.UnhandledException(__target, __exception__);"), + isHresult ? ParseStatement("return unchecked((int)0x80004005u);") + : isVoidReturn ? EmptyStatement() : ParseStatement("return default;") + )))) + .WithBlock(Block(backBodyBlock)) + ); + if (isHresult) + backBodyBlock = backBodyBlock.AddStatements(ParseStatement("return 0;")); + + + backBodyBlock = Block() + .AddStatements(ParseStatement($"{iface.Identifier.Text} __target = null;")) + .AddStatements(backBodyBlock.Statements.ToArray()); + + shadowMethod = shadowMethod.WithBody(backBodyBlock); + + vtbl = vtbl.AddMembers(shadowDelegate).AddMembers(shadowMethod); + vtblCtor.Add(ParseStatement("base.AddMethod((" + shadowDelegate.Identifier.Text + ")" + + shadowMethod.Identifier.Text + ");")); + + + + + } + + class LocalInteropHelper + { + public ClassDeclarationSyntax Class { get; private set; } = ClassDeclaration("LocalInterop"); + private HashSet _existing = new HashSet(); + + public ExpressionSyntax GetCaller(string returnType, List args) + { + var name = "CalliStdCall" + returnType.Replace("*", "_ptr"); + var signature = returnType + "::" + name + "::" + string.Join("::", args); + if (_existing.Add(signature)) + { + Class = Class.AddMembers(MethodDeclaration(ParseTypeName(returnType), name) + .AddModifiers(SyntaxKind.StaticKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PublicKeyword) + .AddParameterListParameters(Parameter(Identifier("thisObj")).WithType(ParseTypeName("void*"))) + .AddParameterListParameters(args.Select((x, i) => + Parameter(Identifier("arg" + i)).WithType(ParseTypeName(x))).ToArray()) + .AddParameterListParameters(Parameter(Identifier("methodPtr")).WithType(ParseTypeName("void*"))) + .WithBody(Block(ExpressionStatement(ThrowExpression(ParseExpression("null")))))); + } + + return ParseExpression("LocalInterop." + name); + } + } + + + void GenerateInterface(ref NamespaceDeclarationSyntax ns, ref NamespaceDeclarationSyntax implNs, + AstInterfaceNode iface) + { + var guidString = iface.Attributes.FirstOrDefault(x => x.Name == "uuid")?.Value; + if (guidString == null) + throw new CodeGenException("Missing GUID for " + iface.Name); + var inheritsUnknown = iface.Inherits == null || iface.Inherits == "IUnknown"; + + var ifaceDec = InterfaceDeclaration(iface.Name) + .WithBaseType(inheritsUnknown ? "Avalonia.MicroCom.IUnknown" : iface.Inherits) + .AddModifiers(Token(_visibility), Token(SyntaxKind.UnsafeKeyword)); + + var proxyClassName = "__MicroCom" + iface.Name + "Proxy"; + var proxy = ClassDeclaration(proxyClassName) + .AddModifiers(Token(SyntaxKind.UnsafeKeyword), Token(_visibility)) + .WithBaseType(inheritsUnknown ? + "Avalonia.MicroCom.MicroComProxyBase" : + ("__MicroCom" + iface.Inherits + "Proxy")); + + + // Generate vtable + var vtbl = ClassDeclaration("__MicroCom" + iface.Name + "VTable") + .AddModifiers(Token(SyntaxKind.UnsafeKeyword)); + + vtbl = vtbl.WithBaseType(inheritsUnknown ? + "Avalonia.MicroCom.MicroComVtblBase" : + "__MicroCom" + iface.Inherits + "VTable"); + + var vtblCtor = new List(); + for (var idx = 0; idx < iface.Count; idx++) + GenerateInterfaceMember(iface[idx], ref ifaceDec, ref proxy, ref vtbl, vtblCtor, idx); + + vtbl = vtbl.AddMembers( + ConstructorDeclaration(vtbl.Identifier.Text) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .WithBody(Block(vtblCtor)) + ) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "__MicroComModuleInit") + .AddModifiers(Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.InternalKeyword)) + .WithExpressionBody(ArrowExpressionClause( + ParseExpression("Avalonia.MicroCom.MicroComRuntime.RegisterVTable(typeof(" + + proxyClassName + "), new " + vtbl.Identifier.Text + "().CreateVTable())"))) + .WithSemicolonToken(Semicolon())); + + + // Finalize proxy code + proxy = proxy.AddMembers( + MethodDeclaration(ParseTypeName("void"), "__MicroComModuleInit") + .AddModifiers(Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.InternalKeyword)) + .WithBody(Block( + ParseStatement("Avalonia.MicroCom.MicroComRuntime.Register(typeof(" + + proxyClassName + "), new Guid(\"" + guidString + "\"), (p, owns) => new " + + proxyClassName + "(p, owns));") + ))) + .AddMembers(ParseMemberDeclaration("public " + proxyClassName + + "(IntPtr nativePointer, bool ownsHandle) : base(nativePointer, ownsHandle) {}")) + .AddMembers(ParseMemberDeclaration("protected override int VTableSize => base.VTableSize + " + + iface.Count + ";")); + + ns = ns.AddMembers(RewriteMethodsToProperties(ifaceDec)); + implNs = implNs.AddMembers(RewriteMethodsToProperties(proxy), RewriteMethodsToProperties(vtbl)); + } + } +} diff --git a/src/tools/MicroComGenerator/CSharpGen.Utils.cs b/src/tools/MicroComGenerator/CSharpGen.Utils.cs new file mode 100644 index 0000000000..ed2bcdd6d8 --- /dev/null +++ b/src/tools/MicroComGenerator/CSharpGen.Utils.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Formatting; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace MicroComGenerator +{ + public partial class CSharpGen + { + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", "System.Text", "System.Collections", "System.Collections.Generic", "Avalonia.MicroCom" + } + .Concat(_extraUsings).Select(u => UsingDirective(IdentifierName(u))))); + + string Format(CompilationUnitSyntax unit) + { + var cw = new AdhocWorkspace(); + return Microsoft.CodeAnalysis.Formatting.Formatter.Format(unit.NormalizeWhitespace(), cw, cw.Options + .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInObjectInit, true) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInObjectCollectionArrayInitializers, true) + .WithChangedOption(CSharpFormattingOptions.NewLineForMembersInAnonymousTypes, true) + .WithChangedOption(CSharpFormattingOptions.NewLinesForBracesInMethods, true) + + ).ToFullString(); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + static VariableDeclarationSyntax DeclareVar(string type, string name, + ExpressionSyntax? initializer = null) + => VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList(VariableDeclarator(name) + .WithInitializer(initializer == null ? null : EqualsValueClause(initializer)))); + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + + bool IsPropertyRewriteCandidate(MethodDeclarationSyntax method) + { + if(method.Identifier.Text.Contains("GetScaling")) + Console.WriteLine(); + return (method.Identifier.Text.StartsWith("Get") && + method.ParameterList.Parameters.Count == 0); + } + + TypeDeclarationSyntax RewriteMethodsToProperties(T decl) where T : TypeDeclarationSyntax + { + var replace = new Dictionary(); + foreach (var method in decl.Members.OfType().ToList()) + { + if (IsPropertyRewriteCandidate(method)) + { + var getter = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); + if (method.Body != null) + getter = getter.WithBody(method.Body); + else + getter = getter.WithSemicolonToken(Semicolon()); + + replace[method] = PropertyDeclaration(method.ReturnType, + method.Identifier.Text.Substring(3)) + .WithModifiers(method.Modifiers).AddAccessorListAccessors(getter); + + } + } + + return decl.ReplaceNodes(replace.Keys, (m, m2) => replace[m]); + } + + } +} diff --git a/src/tools/MicroComGenerator/CSharpGen.cs b/src/tools/MicroComGenerator/CSharpGen.cs new file mode 100644 index 0000000000..49e4f7a09e --- /dev/null +++ b/src/tools/MicroComGenerator/CSharpGen.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using MicroComGenerator.Ast; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +// ReSharper disable CoVariantArrayConversion + +namespace MicroComGenerator +{ + public partial class CSharpGen + { + private readonly AstIdlNode _idl; + private List _extraUsings; + private string _namespace; + private SyntaxKind _visibility; + private LocalInteropHelper _localInterop = new LocalInteropHelper(); + + public CSharpGen(AstIdlNode idl) + { + _idl = idl; + _extraUsings = _idl.Attributes.Where(u => u.Name == "clr-using").Select(u => u.Value).ToList(); + _namespace = _idl.Attributes.FirstOrDefault(x => x.Name == "clr-namespace")?.Value; + if (_namespace == null) + throw new CodeGenException("Missing clr-namespace attribute"); + var visibilityString = _idl.Attributes.FirstOrDefault(x => x.Name == "clr-access")?.Value; + if (visibilityString == null) + throw new CodeGenException("Missing clr-visibility attribute"); + if (visibilityString == "internal") + _visibility = SyntaxKind.InternalKeyword; + else if (visibilityString == "public") + _visibility = SyntaxKind.PublicKeyword; + else + throw new CodeGenException("Invalid clr-access attribute"); + } + + public string Generate() + { + var ns = NamespaceDeclaration(ParseName(_namespace)); + var implNs = NamespaceDeclaration(ParseName(_namespace + ".Impl")); + ns = GenerateEnums(ns); + ns = GenerateStructs(ns); + foreach (var i in _idl.Interfaces) + GenerateInterface(ref ns, ref implNs, i); + + implNs = implNs.AddMembers(_localInterop.Class); + var unit = Unit().AddMembers(ns, implNs); + + return Format(unit); + } + + NamespaceDeclarationSyntax GenerateEnums(NamespaceDeclarationSyntax ns) + { + return ns.AddMembers(_idl.Enums.Select(e => + EnumDeclaration(e.Name) + .WithModifiers(TokenList(Token(_visibility))) + .WithMembers(SeparatedList(e.Select(m => + { + var member = EnumMemberDeclaration(m.Name); + if (m.Value != null) + return member.WithEqualsValue(EqualsValueClause(ParseExpression(m.Value))); + return member; + }))) + ).ToArray()); + } + + NamespaceDeclarationSyntax GenerateStructs(NamespaceDeclarationSyntax ns) + { + return ns.AddMembers(_idl.Structs.Select(e => + StructDeclaration(e.Name) + .WithModifiers(TokenList(Token(_visibility))) + .AddModifiers(Token(SyntaxKind.UnsafeKeyword)) + .AddAttributeLists(AttributeList(SingletonSeparatedList( + Attribute(ParseName("System.Runtime.InteropServices.StructLayout"), + AttributeArgumentList(SingletonSeparatedList( + AttributeArgument( + ParseExpression("System.Runtime.InteropServices.LayoutKind.Sequential")))) + )))) + .WithMembers(new SyntaxList(SeparatedList(e.Select(m => + DeclareField(m.Type.ToString(), m.Name, SyntaxKind.PublicKeyword))))) + ).ToArray()); + } + + + + } +} diff --git a/src/tools/MicroComGenerator/CppGen.cs b/src/tools/MicroComGenerator/CppGen.cs new file mode 100644 index 0000000000..133902f764 --- /dev/null +++ b/src/tools/MicroComGenerator/CppGen.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Text; +using MicroComGenerator.Ast; + +namespace MicroComGenerator +{ + public class CppGen + { + static string ConvertType(AstTypeNode type) + { + var name = type.Name; + if (name == "byte") + name = "unsigned char"; + else if(name == "uint") + name = "unsigned int"; + return name + new string('*', type.PointerLevel); + } + + public static string GenerateCpp(AstIdlNode idl) + { + var sb = new StringBuilder(); + var preamble = idl.Attributes.FirstOrDefault(x => x.Name == "cpp-preamble")?.Value; + if (preamble != null) + sb.AppendLine(preamble); + + foreach (var s in idl.Structs) + sb.AppendLine("struct " + s.Name + ";"); + + foreach (var s in idl.Interfaces) + sb.AppendLine("struct " + s.Name + ";"); + + foreach (var en in idl.Enums) + { + sb.Append("enum "); + if (en.Attributes.Any(a => a.Name == "class-enum")) + sb.Append("class "); + sb.Append(en.Name).Append(" "); + sb.AppendLine(en.Name).AppendLine("{"); + + foreach (var m in en) + { + sb.Append(" ").Append(m.Name); + if (m.Value != null) + sb.Append(" = ").Append(m.Value); + sb.AppendLine(","); + } + + sb.AppendLine("};"); + } + + foreach (var s in idl.Structs) + { + sb.Append("struct ").AppendLine(s.Name).AppendLine("{"); + foreach (var m in s) + sb.Append(" ").Append(ConvertType(m.Type)).Append(" ").Append(m.Name).AppendLine(";"); + + sb.AppendLine("};"); + } + + foreach (var i in idl.Interfaces) + { + var guidString = i.Attributes.FirstOrDefault(x => x.Name == "uuid")?.Value; + if (guidString == null) + throw new CodeGenException("Missing uuid for " + i.Name); + + var guid = Guid.Parse(guidString).ToString().Replace("-", ""); + + + sb.Append("COMINTERFACE(").Append(i.Name).Append(", ") + .Append(guid.Substring(0, 8)).Append(", ") + .Append(guid.Substring(8, 4)).Append(", ") + .Append(guid.Substring(12, 4)); + for (var c = 0; c < 8; c++) + { + sb.Append(", ").Append(guid.Substring(16 + c * 2, 2)); + } + + sb.Append(") : ") + .AppendLine(i.Inherits ?? "IUnknown") + .AppendLine("{"); + + foreach (var m in i) + { + sb.Append(" ").Append(ConvertType(m.ReturnType)).Append(" ").Append(m.Name).AppendLine(" ("); + for (var c = 0; c < m.Count; c++) + { + var arg = m[c]; + sb.Append(" "); + if (arg.Attributes.Any(a => a.Name == "const")) + sb.Append("const "); + sb.Append(ConvertType(arg.Type)) + .Append(" ") + .Append(arg.Name); + if (c != m.Count - 1) + sb.Append(", "); + sb.AppendLine(); + sb.AppendLine(" );"); + } + } + + sb.AppendLine("}"); + } + + return sb.ToString(); + } + } +} diff --git a/src/tools/MicroComGenerator/Extensions.cs b/src/tools/MicroComGenerator/Extensions.cs new file mode 100644 index 0000000000..4942442d1b --- /dev/null +++ b/src/tools/MicroComGenerator/Extensions.cs @@ -0,0 +1,90 @@ + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace MicroComGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static InterfaceDeclarationSyntax WithBaseType(this InterfaceDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} diff --git a/src/tools/MicroComGenerator/MicroComGenerator.csproj b/src/tools/MicroComGenerator/MicroComGenerator.csproj new file mode 100644 index 0000000000..193bb9a100 --- /dev/null +++ b/src/tools/MicroComGenerator/MicroComGenerator.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp3.1 + + + + + + + diff --git a/src/tools/MicroComGenerator/ParseException.cs b/src/tools/MicroComGenerator/ParseException.cs new file mode 100644 index 0000000000..dfa8dcfe54 --- /dev/null +++ b/src/tools/MicroComGenerator/ParseException.cs @@ -0,0 +1,27 @@ +using System; + +namespace MicroComGenerator +{ + class ParseException : Exception + { + public int Line { get; } + public int Position { get; } + + public ParseException(string message, int line, int position) : base(message) + { + Line = line; + Position = position; + } + + public ParseException(string message, ref TokenParser parser) : this(message, parser.Line, parser.Position) + { + } + } + + class CodeGenException : Exception + { + public CodeGenException(string message) : base(message) + { + } + } +} diff --git a/src/tools/MicroComGenerator/Program.cs b/src/tools/MicroComGenerator/Program.cs new file mode 100644 index 0000000000..578ba1465d --- /dev/null +++ b/src/tools/MicroComGenerator/Program.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Linq; +using CommandLine; + +namespace MicroComGenerator +{ + class Program + { + public class Options + { + [Option('i', "input", Required = true, HelpText = "Input IDL file")] + public string Input { get; set; } + + [Option("cpp", Required = false, HelpText = "C++ output file")] + public string CppOutput { get; set; } + + [Option("cs", Required = false, HelpText = "C# output file")] + public string CSharpOutput { get; set; } + + } + + static int Main(string[] args) + { + var p = Parser.Default.ParseArguments(args); + if (p is NotParsed) + { + return 1; + } + + var opts = ((Parsed)p).Value; + + var text = File.ReadAllText(opts.Input); + var ast = AstParser.Parse(text); + + if (opts.CppOutput != null) + File.WriteAllText(opts.CppOutput, CppGen.GenerateCpp(ast)); + if (opts.CSharpOutput != null) + File.WriteAllText(opts.CSharpOutput, new CSharpGen(ast).Generate()); + + return 0; + } + } +} diff --git a/src/tools/MicroComGenerator/TokenParser.cs b/src/tools/MicroComGenerator/TokenParser.cs new file mode 100644 index 0000000000..ea8850b8e4 --- /dev/null +++ b/src/tools/MicroComGenerator/TokenParser.cs @@ -0,0 +1,417 @@ +using System; +using System.Globalization; +using System.IO; + +namespace MicroComGenerator +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public int Line { get; private set; } + public TokenParser(ReadOnlySpan s) + { + _s = s; + Position = 0; + Line = 0; + } + + public void SkipWhitespace() + { + while (true) + { + if(_s.Length == 0) + return; + if (char.IsWhiteSpace(_s[0])) + Advance(1); + else if (_s[0] == '/' && _s.Length>1) + { + if (_s[1] == '/') + SkipOneLineComment(); + else if (_s[1] == '*') + SkipMultiLineComment(); + else + return; + } + else + return; + } + } + + void SkipOneLineComment() + { + while (true) + { + if (_s.Length > 0 && _s[0] != '\n' && _s[0] != '\r') + Advance(1); + else + return; + } + } + + void SkipMultiLineComment() + { + var l = Line; + var p = Position; + while (true) + { + if (_s.Length == 0) + throw new ParseException("No matched */ found for /*", l, p); + + if (_s[0] == '*' && _s.Length > 1 && _s[1] == '/') + { + Advance(2); + return; + } + + Advance(1); + } + } + + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z'); + + public void Consume(char c) + { + if (!TryConsume(c)) + throw new ParseException("Expected " + c, Line, Position); + } + public bool TryConsume(char c) + { + SkipWhitespace(); + if (_s.Length == 0 || _s[0] != c) + return false; + + Advance(1); + return true; + } + + public bool TryConsume(string s) + { + SkipWhitespace(); + if (_s.Length < s.Length) + return false; + for (var c = 0; c < s.Length; c++) + { + if (_s[c] != s[c]) + return false; + } + + Advance(s.Length); + return true; + } + + public bool TryConsumeAny(ReadOnlySpan chars, out char token) + { + SkipWhitespace(); + token = default; + if (_s.Length == 0) + return false; + + foreach (var c in chars) + { + if (c == _s[0]) + { + token = c; + Advance(1); + return true; + } + } + + return false; + } + + + public bool TryParseKeyword(string keyword) + { + SkipWhitespace(); + if (keyword.Length > _s.Length) + return false; + for(var c=0; c keyword.Length && IsAlphaNumeric(_s[keyword.Length])) + return false; + + Advance(keyword.Length); + return true; + } + + public bool TryParseKeywordLowerCase(string keywordInLowerCase) + { + SkipWhitespace(); + if (keywordInLowerCase.Length > _s.Length) + return false; + for(var c=0; c keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) + return false; + + Advance(keywordInLowerCase.Length); + return true; + } + + public void Advance(int c) + { + while (c > 0) + { + if (_s[0] == '\n') + { + Line++; + Position = 0; + } + else + Position++; + + _s = _s.Slice(1); + c--; + } + } + + public int Length => _s.Length; + public bool Eof + { + get + { + SkipWhitespace(); + return Length == 0; + } + } + + public char Peek + { + get + { + if (_s.Length == 0) + throw new ParseException("Unexpected EOF", Line, Position); + return _s[0]; + } + } + + public string ParseIdentifier(ReadOnlySpan extraValidChars) + { + if (!TryParseIdentifier(extraValidChars, out var ident)) + throw new ParseException("Identifier expected", Line, Position); + return ident.ToString(); + } + + public string ParseIdentifier() + { + if (!TryParseIdentifier(out var ident)) + throw new ParseException("Identifier expected", Line, Position); + return ident.ToString(); + } + + public bool TryParseIdentifier(ReadOnlySpan extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_')) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch) || ch == '_') + len++; + else + { + var found = false; + foreach(var vc in extraValidChars) + if (vc == ch) + { + found = true; + break; + } + + if (found) + len++; + else + break; + } + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public bool TryParseIdentifier(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_')) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if (IsAlphaNumeric(ch) || ch == '_') + len++; + else + break; + } + + res = _s.Slice(0, len); + Advance(len); + return true; + } + + public string ReadToEol() + { + var initial = _s; + var len = 0; + while (true) + { + if (_s.Length > 0 && _s[0] != '\n' && _s[0] != '\r') + { + len++; + Advance(1); + } + else + return initial.Slice(0, len).ToString(); + } + } + + public string ReadTo(char c) + { + var initial = _s; + var len = 0; + var l = Line; + var p = Position; + while (true) + { + if (_s.Length == 0) + throw new ParseException("Expected " + c + " before EOF", l, p); + + if (_s[0] != c) + { + len++; + Advance(1); + } + else + return initial.Slice(0, len).ToString(); + } + } + + public string ReadToAny(ReadOnlySpan chars) + { + var initial = _s; + var len = 0; + var l = Line; + var p = Position; + while (true) + { + if (_s.Length == 0) + throw new ParseException("Expected any of '" + chars.ToString() + "' before EOF", l, p); + + var foundTerminator = false; + foreach (var term in chars) + { + if (_s[0] == term) + { + foundTerminator = true; + break; + } + } + + if (!foundTerminator) + { + len++; + Advance(1); + } + else + return initial.Slice(0, len).ToString(); + } + } + + public bool TryParseCall(out ReadOnlySpan res) + { + res = ReadOnlySpan.Empty; + SkipWhitespace(); + if (_s.Length == 0) + return false; + var first = _s[0]; + if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) + return false; + int len = 1; + for (var c = 1; c < _s.Length; c++) + { + var ch = _s[c]; + if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') + len++; + else + break; + } + + res = _s.Slice(0, len); + + // Find '(' + for (var c = len; c < _s.Length; c++) + { + if(char.IsWhiteSpace(_s[c])) + continue; + if(_s[c]=='(') + { + Advance(c + 1); + return true; + } + + return false; + + } + + return false; + + } + + + public bool TryParseFloat(out float res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + + public override string ToString() => _s.ToString(); + + } +}