From fd2b990a8fd1b1b8d69ac93744ab19ca02d3c7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Sun, 16 Aug 2026 14:30:24 +0200 Subject: [PATCH] Reuse running Visual Studio instances --- .../src/CodeEditors/EnvDte/DteOpenRequest.bf | 127 ++++++++++ .../src/CodeEditors/EnvDte/VisualStudioDte.bf | 236 ++++++++++++++++++ .../src/CodeEditors/VisualStudioIdeAdapter.bf | 83 +++--- .../src/Platform/Windows/Com/ComDispatch.bf | 150 +++++++++++ .../src/Platform/Windows/Com/OleAutomation.bf | 211 ++++++++++++++++ .../Windows/Com/RunningObjectTable.bf | 107 ++++++++ GlitchyEngine/vendor/directx | 2 +- 7 files changed, 873 insertions(+), 43 deletions(-) create mode 100644 GlitchyEditor/src/CodeEditors/EnvDte/DteOpenRequest.bf create mode 100644 GlitchyEditor/src/CodeEditors/EnvDte/VisualStudioDte.bf create mode 100644 GlitchyEditor/src/Platform/Windows/Com/ComDispatch.bf create mode 100644 GlitchyEditor/src/Platform/Windows/Com/OleAutomation.bf create mode 100644 GlitchyEditor/src/Platform/Windows/Com/RunningObjectTable.bf diff --git a/GlitchyEditor/src/CodeEditors/EnvDte/DteOpenRequest.bf b/GlitchyEditor/src/CodeEditors/EnvDte/DteOpenRequest.bf new file mode 100644 index 0000000..e487193 --- /dev/null +++ b/GlitchyEditor/src/CodeEditors/EnvDte/DteOpenRequest.bf @@ -0,0 +1,127 @@ +#if BF_PLATFORM_WINDOWS + +using System; +using System.Diagnostics; +using System.Threading; +using DirectX.Common; +using GlitchyEngine; +using GlitchyEditor.Platform.Windows.Com; + +namespace GlitchyEditor.CodeEditors; + +/// A request to open the script solution (or a file in it) in Visual Studio. +/// Runs on its own worker thread so the editor never blocks on a busy or starting Visual Studio. +class DteOpenRequest +{ + private const int ColdStartMaxPolls = 120; + private const int ColdStartPollIntervalMs = 500; + + private append String _solutionPath = .(); + private append String _devenvPath = .(); + private append String _fileName = .(); + + public StringView SolutionPath + { + get => _solutionPath; + set => _solutionPath.Set(value); + } + + public StringView DevenvPath + { + get => _devenvPath; + set => _devenvPath.Set(value); + } + + /// Leave empty to just open the project. + public StringView FileName + { + get => _fileName; + set => _fileName.Set(value); + } + + public int LineNumber; + + private static int32 sWorkerActive = 0; + + public static void Start(DteOpenRequest ownRequest) + { + if (Interlocked.CompareExchange(ref sWorkerActive, 0, 1) != 0) + { + Log.EngineLogger.Info("Visual Studio is already being opened, ignoring the request."); + delete ownRequest; + return; + } + + Thread workerThread = new Thread(new () => + { + ownRequest.Run(); + delete ownRequest; + + Interlocked.Exchange(ref sWorkerActive, 0); + }); + workerThread.SetName("Visual Studio DTE worker"); + // Don't block editor shutdown on a worker that is still polling; skipping the COM cleanup + // at process exit is harmless. + workerThread.IsBackground = true; + workerThread.Start(true); + } + + private void Run() + { + // The worker does its own COM initialization, the editor main thread (STA via OleInitialize) + // stays untouched. + if (CoInitializeEx(null, COINIT_APARTMENTTHREADED).Failed(let result)) + { + Log.EngineLogger.Error(scope $"CoInitializeEx failed: {result} ({(uint32)result:X8})"); + return; + } + + defer CoUninitialize(); + + if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let dte)) + { + if (FileName.IsEmpty) + VisualStudioDte.ActivateMainWindow(dte); + else + VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber).IgnoreError(); + + delete dte; + + return; + } + + Log.EngineLogger.Info(scope $"No running Visual Studio instance has \"{SolutionPath}\" open, starting a new one."); + + ProcessStartInfo startInfo = scope .(); + startInfo.SetFileName(DevenvPath); + startInfo.SetArguments(scope $"\"{SolutionPath}\""); + + if (scope SpawnedProcess().Start(startInfo) case .Err) + { + Log.EngineLogger.Error(scope $"Failed to start Visual Studio (\"{DevenvPath}\")."); + return; + } + + if (FileName.IsEmpty) + return; + + // Wait for the new instance to finish loading the solution (Solution.FullName stays empty + // until then), then open the file in it. + for (int i < ColdStartMaxPolls) + { + Thread.Sleep(ColdStartPollIntervalMs); + + if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let startedDte)) + { + VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber).IgnoreError(); + + delete startedDte; + return; + } + } + + Log.EngineLogger.Warning(scope $"Visual Studio was started but its automation interface never became available, \"{FileName}\" was not opened automatically."); + } +} + +#endif diff --git a/GlitchyEditor/src/CodeEditors/EnvDte/VisualStudioDte.bf b/GlitchyEditor/src/CodeEditors/EnvDte/VisualStudioDte.bf new file mode 100644 index 0000000..0cd013d --- /dev/null +++ b/GlitchyEditor/src/CodeEditors/EnvDte/VisualStudioDte.bf @@ -0,0 +1,236 @@ +#if BF_PLATFORM_WINDOWS + +using System; +using System.Threading; +using DirectX.Common; +using GlitchyEngine; +using GlitchyEditor.Platform.Windows.Com; +using System.IO; + +namespace GlitchyEditor.CodeEditors; + +/// Talks to running Visual Studio instances via their EnvDTE automation objects, which are +/// published in the Running Object Table as "!VisualStudio.DTE.:". +/// All methods must be called from a thread that is initialized for COM (CoInitializeEx). +static class VisualStudioDte +{ + private const String MonikerPrefix = "!VisualStudio.DTE"; + /// EnvDTE.Constants.vsViewKindCode + private const String ViewKindCode = "{7651A701-06E5-11D1-8EBD-00A0C90F26EA}"; + + /// Searches the Running Object Table for a Visual Studio instance that has the given solution open. + public static Result FindRunningInstance(StringView solutionPath) + { + IRunningObjectTable* rot = null; + if (GetRunningObjectTable(0, out rot).Failed(var result) || rot == null) + { + Log.EngineLogger.Error(scope $"GetRunningObjectTable failed: {result} ({(uint32)result:X8})"); + return .Err; + } + + defer rot.Release(); + + IBindCtx* bindCtx = null; + if (CreateBindCtx(0, out bindCtx).Failed(out result) || bindCtx == null) + { + Log.EngineLogger.Error(scope $"CreateBindCtx failed: {result} ({(uint32)result:X8})"); + return .Err; + } + + defer bindCtx.Release(); + + IEnumMoniker* enumMoniker = null; + if (rot.EnumRunning(out enumMoniker).Failed(out result) || enumMoniker == null) + { + Log.EngineLogger.Error(scope $"IRunningObjectTable.EnumRunning failed: {result} ({(uint32)result:X8})"); + return .Err; + } + + defer enumMoniker.Release(); + + IMoniker* moniker = null; + while (enumMoniker.Next(1, out moniker, ?) == .S_OK) + { + ComDispatch dte = GetDteFromMoniker(rot, bindCtx, moniker); + + ReleaseAndNullify!(moniker); + + if (dte == null) + continue; + + String solutionFullName = scope .(); + + if (TryGetSolutionFullName(dte, solutionFullName)) + { + Log.EngineLogger.Trace(scope $"Found running Visual Studio instance with solution \"{solutionFullName}\"."); + + if (SolutionPathsEqual(solutionFullName, solutionPath)) + return dte; + } + + // No match, cleanup. + delete dte; + } + + return .Err; + } + + /// Brings the main window of the given Visual Studio instance to the foreground. + public static void ActivateMainWindow(ComDispatch dte) + { + if (!(dte.GetObjectProperty("MainWindow") case .Ok(let mainWindow))) + { + Log.EngineLogger.Warning("Failed to get the Visual Studio main window."); + return; + } + + defer delete mainWindow; + + if (mainWindow.InvokeMethod("Activate") case .Ok(var activateResult)) + VariantClear(&activateResult); + + // Activate alone often isn't enough to raise a window of another process, + // so additionally restore and foreground it via its HWND. + if (mainWindow.GetProperty("HWnd") case .Ok(var hwndValue)) + { + if (hwndValue.GetHwnd() case .Ok(let hwnd)) + { + if (IsIconic(hwnd)) + ShowWindow(hwnd, SW_RESTORE); + + SetForegroundWindow(hwnd); + } + + VariantClear(&hwndValue); + } + } + + /// Opens the given file in the code editor of the given Visual Studio instance and + /// jumps to lineNumber (1-based, ignored if <= 0). + public static Result OpenFileAtLine(ComDispatch dte, StringView fileName, int lineNumber) + { + ComDispatch itemOperations = Try!(dte.GetObjectProperty("ItemOperations")); + defer delete itemOperations; + + VARIANT[] openFileArgs = scope .(VARIANT.FromBStr(fileName), VARIANT.FromBStr(ViewKindCode)); + + if (itemOperations.InvokeMethod("OpenFile", openFileArgs) case .Ok(var window)) + { + VariantClear(&window); + } + else + { + Log.EngineLogger.Error(scope $"Failed to open \"{fileName}\" in Visual Studio."); + return .Err; + } + + if (lineNumber > 0) + GoToLine(dte, lineNumber); + + ActivateMainWindow(dte); + + return .Ok; + } + + private static ComDispatch GetDteFromMoniker(IRunningObjectTable* rot, IBindCtx* bindCtx, IMoniker* moniker) + { + char16* displayNameW = null; + + if (moniker.GetDisplayName(bindCtx, null, out displayNameW).Failed || displayNameW == null) + return null; + + String displayName = scope .(); + displayName.Append(displayNameW); + CoTaskMemFree(displayNameW); + + if (!displayName.StartsWith(MonikerPrefix)) + return null; + + IUnknown* unknown = null; + defer { unknown?.Release(); } + + if (rot.GetObject(moniker, out unknown).Failed || unknown == null) + return null; + + IDispatch* dispatch = null; + + if (unknown.QueryInterface(out dispatch).Succeeded) + return new ComDispatch(dispatch); + + return null; + } + + private static bool TryGetSolutionFullName(ComDispatch dte, String outFullName) + { + if (!(dte.GetObjectProperty("Solution") case .Ok(let solution))) + return false; + + defer delete solution; + + return solution.GetStringProperty("FullName", outFullName) case .Ok; + } + + private static bool SolutionPathsEqual(StringView left, StringView right) + { + String normalizedLeft = NormalizePath(left, .. scope .()); + String normalizedRight = NormalizePath(right, .. scope .()); + + // Be tolerant about the solution extension: it isn't guaranteed that the DTE reports + // the same extension for .slnx solutions that we expect. + return StripSolutionExtension(normalizedLeft) == StripSolutionExtension(normalizedRight); + } + + private static void NormalizePath(StringView path, String output) + { + output.Append(path); + output.Replace('/', '\\'); + output.ToLower(); + } + + private static StringView StripSolutionExtension(StringView path) + { + if (path.EndsWith(".slnx")) + return path.Substring(0, path.Length - ".slnx".Length); + + if (path.EndsWith(".sln")) + return path.Substring(0, path.Length - ".sln".Length); + + return path; + } + + private static void GoToLine(ComDispatch dte, int lineNumber) + { + ComDispatch document = null; + defer { delete document; } + + // Directly after OpenFile the ActiveDocument might not be set yet, so retry for a bit. + for (int i < 10) + { + if (dte.GetObjectProperty("ActiveDocument") case .Ok(out document)) + break; + + Thread.Sleep(100); + } + + if (document == null) + { + Log.EngineLogger.Warning("Could not jump to the requested line: no active document."); + return; + } + + if (document.GetObjectProperty("Selection") not case .Ok(let selection)) + { + Log.EngineLogger.Warning("Could not jump to the requested line: failed to get the text selection."); + return; + } + + VARIANT[2] gotoLineArgs = .(VARIANT.FromInt32((int32)lineNumber), VARIANT.FromBool(false)); + + if (selection.InvokeMethod("GotoLine", gotoLineArgs) case .Ok(var gotoResult)) + VariantClear(&gotoResult); + + delete selection; + } +} + +#endif diff --git a/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf b/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf index b4c58ef..b130209 100644 --- a/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf +++ b/GlitchyEditor/src/CodeEditors/VisualStudioIdeAdapter.bf @@ -1,7 +1,8 @@ using System; using System.Diagnostics; +using System.IO; using GlitchyEngine; -using System.Collections; +using GlitchyEditor.EditWindows; using GlitchyEditor.Settings; namespace GlitchyEditor.CodeEditors; @@ -17,55 +18,53 @@ class VisualStudioIdeAdapter : IIdeAdapter _ideInstallation = ideInstallation; } - public bool IsRunning() - { - List processes = scope .(); - - if (Process.GetProcesses(processes) case .Err) - { - Log.EngineLogger.Error("Failed to get running processes."); - return false; - } - - for (Process process in processes) - { - // TODO? - } - - ClearAndDeleteItems!(processes); - - return false; - } - public void OpenScript(StringView fileName, int lineNumber) { - if (IsRunning()) - { - ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(_ideInstallation.Path); - startInfo.SetArguments(scope $"/Edit {fileName}"); - - scope SpawnedProcess().Start(startInfo); - } - else - { - ProcessStartInfo startInfo = scope .(); - startInfo.SetFileName(_ideInstallation.Path); - startInfo.SetArguments(scope $"/Edit {fileName}"); - - scope SpawnedProcess().Start(startInfo); - } + Open(fileName, lineNumber); } public void OpenScriptProject() { - String solutionPath = scope .(); + Open(null, 0); + } + + private void Open(StringView fileName, int lineNumber) + { + String solutionPath = scope String(); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); - ProcessStartInfo psi = scope .(); - psi.SetFileName(_ideInstallation.Path); - psi.SetArguments(solutionPath); + if (!File.Exists(solutionPath)) + { + Log.EngineLogger.Error(scope $"Could not find solution file \"{solutionPath}\"."); + return; + } - scope SpawnedProcess().Start(psi); + if (!File.Exists(_ideInstallation.Path)) + { + Editor.Instance.ShowSettings(); + Editor.Instance.SettingsWindow.HighlightSetting("Tools", "IDE"); + + PopupService.Instance.ShowMessageBox("Visual Studio not found.", + "The Visual Studio path could not be found. Please select a different IDE."); + return; + } + +#if BF_PLATFORM_WINDOWS + // Reuses a running Visual Studio instance that has our solution open (via EnvDTE), + // otherwise starts a new one. + DteOpenRequest request = new .(); + request.SolutionPath = solutionPath; + request.DevenvPath = _ideInstallation.Path; + request.FileName = fileName; + request.LineNumber = lineNumber; + + DteOpenRequest.Start(request); +#else + ProcessStartInfo startInfo = scope .(); + startInfo.SetFileName(_ideInstallation.Path); + startInfo.SetArguments(scope $"\"{solutionPath}\""); + + scope SpawnedProcess().Start(startInfo); +#endif } } diff --git a/GlitchyEditor/src/Platform/Windows/Com/ComDispatch.bf b/GlitchyEditor/src/Platform/Windows/Com/ComDispatch.bf new file mode 100644 index 0000000..f3a2311 --- /dev/null +++ b/GlitchyEditor/src/Platform/Windows/Com/ComDispatch.bf @@ -0,0 +1,150 @@ +#if BF_PLATFORM_WINDOWS + +using System; +using System.Threading; +using DirectX.Common; +using GlitchyEngine; + +namespace GlitchyEditor.Platform.Windows.Com; + +/// Owns an IDispatch pointer and provides late bound access to its properties and methods +/// (GetIDsOfNames + Invoke). Calls that are rejected because the COM server is busy +/// (e.g. Visual Studio showing a modal dialog or building) are retried for a bounded time. +class ComDispatch +{ + private const int MaxBusyRetries = 20; + private const int BusyRetrySleepMs = 150; + + private static Guid sIID_NULL = .(); + + private IDispatch* _dispatch; + + /// Takes ownership of the reference held by dispatch. + public this(IDispatch* dispatch) + { + _dispatch = dispatch; + } + + public ~this() + { + if (_dispatch != null) + _dispatch.Release(); + } + + /// The caller must release the returned VARIANT with VariantClear. + public Result GetProperty(StringView name) + { + return InvokeInternal(name, .PropertyGet, default); + } + + /// Arguments are passed in natural (declaration) order and are released by this method. + /// The caller must release the returned VARIANT with VariantClear. + public Result InvokeMethod(StringView name, Span args = default) + { + Result result = InvokeInternal(name, .Method, args); + + for (int i < args.Length) + VariantClear(&args[i]); + + return result; + } + + /// Returns the property value as a new ComDispatch (fails if the property is not an object or null). + public Result GetObjectProperty(StringView name) + { + VARIANT value = Try!(GetProperty(name)); + + if (value.vt == .Dispatch && value.pdispVal != null) + { + // The new ComDispatch takes over the reference held by the VARIANT, so no VariantClear here. + return new ComDispatch(value.pdispVal); + } + + VariantClear(&value); + return .Err; + } + + /// Appends the string property value to outValue. + public Result GetStringProperty(StringView name, String outValue) + { + VARIANT value = Try!(GetProperty(name)); + defer VariantClear(&value); + + if (value.vt != .Bstr || value.bstrVal == null) + return .Err; + + outValue.Append(value.bstrVal); + return .Ok; + } + + private Result InvokeInternal(StringView name, DispatchFlags flags, Span args) + { + char16* nameW = name.ToScopedNativeWChar!(); + + int32 dispId = 0; + HResult result = _dispatch.GetIDsOfNames(&sIID_NULL, &nameW, 1, LOCALE_USER_DEFAULT, &dispId); + + if (result.Failed) + { + Log.EngineLogger.Error(scope $"IDispatch.GetIDsOfNames failed for \"{name}\": {result} ({(uint32)result:X8})"); + return .Err; + } + + // IDispatch expects the arguments in reverse order (rgvarg[0] is the last parameter). + VARIANT[] reversedArgs = scope VARIANT[args.Length]; + + for (int i < args.Length) + reversedArgs[i] = args[args.Length - 1 - i]; + + DISPPARAMS dispParams = .() + { + rgvarg = reversedArgs.Ptr, + rgdispidNamedArgs = null, + cArgs = (uint32)args.Length, + cNamedArgs = 0 + }; + + VARIANT returnValue = default; + EXCEPINFO excepInfo = default; + + for (int attempt = 0; true; attempt++) + { + result = _dispatch.Invoke(dispId, &sIID_NULL, LOCALE_USER_DEFAULT, flags, &dispParams, &returnValue, &excepInfo, null); + + if ((result == .RPC_E_CALL_REJECTED || result == .RPC_E_SERVERCALL_RETRYLATER) && attempt < MaxBusyRetries) + { + Thread.Sleep(BusyRetrySleepMs); + continue; + } + + break; + } + + if (result == .DISP_E_EXCEPTION) + { + if (excepInfo.bstrDescription == null && excepInfo.pfnDeferredFillIn != null) + excepInfo.pfnDeferredFillIn(&excepInfo); + + String description = scope .(); + + if (excepInfo.bstrDescription != null) + description.Append(excepInfo.bstrDescription); + + Log.EngineLogger.Error(scope $"IDispatch.Invoke of \"{name}\" threw an exception: \"{description}\" (scode: {(uint32)excepInfo.scode:X8})"); + + excepInfo.FreeStrings(); + + return .Err; + } + + if (result.Failed) + { + Log.EngineLogger.Error(scope $"IDispatch.Invoke of \"{name}\" failed: {result} ({(uint32)result:X8})"); + return .Err; + } + + return returnValue; + } +} + +#endif diff --git a/GlitchyEditor/src/Platform/Windows/Com/OleAutomation.bf b/GlitchyEditor/src/Platform/Windows/Com/OleAutomation.bf new file mode 100644 index 0000000..2f15fcc --- /dev/null +++ b/GlitchyEditor/src/Platform/Windows/Com/OleAutomation.bf @@ -0,0 +1,211 @@ +#if BF_PLATFORM_WINDOWS + +using System; +using DirectX.Common; + +namespace GlitchyEditor.Platform.Windows.Com; + +typealias BSTR = char16*; + +static +{ + public const uint32 COINIT_APARTMENTTHREADED = 0x2; + public const uint32 LOCALE_USER_DEFAULT = 0x0400; + + public const int32 SW_RESTORE = 9; + + [Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern HResult CoInitializeEx(void* reserved, uint32 coInit); + [Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern void CoUninitialize(); + [Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern void CoTaskMemFree(void* pointer); + + [Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern BSTR SysAllocString(char16* str); + [Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern void SysFreeString(BSTR str); + [Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern uint32 SysStringLen(BSTR str); + [Import("OleAut32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern HResult VariantClear(VARIANT* variant); + + [Import("user32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern System.Windows.IntBool SetForegroundWindow(int hwnd); + [Import("user32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern System.Windows.IntBool IsIconic(int hwnd); + [Import("user32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern System.Windows.IntBool ShowWindow(int hwnd, int32 cmdShow); +} + +public enum VarType : uint16 +{ + Empty = 0, + I2 = 2, + I4 = 3, + Bstr = 8, + Dispatch = 9, + Bool = 11, + Unknown = 13, + UI4 = 19, + I8 = 20 +} + +public enum DispatchFlags : uint16 +{ + Method = 0x1, + PropertyGet = 0x2, + PropertyPut = 0x4, + PropertyPutRef = 0x8 +} + +[CRepr] +public struct VARIANT +{ + public const int16 VARIANT_TRUE = -1; + public const int16 VARIANT_FALSE = 0; + + public VarType vt; + public uint16 wReserved1; + public uint16 wReserved2; + public uint16 wReserved3; + public using _Value Value; + + [CRepr, Union] + public struct _Value + { + public int64 llVal; + public int32 lVal; + public int16 iVal; + public int16 boolVal; + public BSTR bstrVal; + public IDispatch* pdispVal; + public IUnknown* punkVal; + // BRECORD is the widest union member and forces the correct union size of 16 bytes. + public _Record brecord; + + [CRepr] + public struct _Record + { + public void* pvRecord; + public void* pRecInfo; + } + } + + /// The returned VARIANT owns the BSTR; release it with VariantClear. + public static VARIANT FromBStr(StringView value) + { + VARIANT variant = default; + variant.vt = .Bstr; + variant.bstrVal = SysAllocString(value.ToScopedNativeWChar!()); + return variant; + } + + public static VARIANT FromInt32(int32 value) + { + VARIANT variant = default; + variant.vt = .I4; + variant.lVal = value; + return variant; + } + + public static VARIANT FromBool(bool value) + { + VARIANT variant = default; + variant.vt = .Bool; + variant.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE; + return variant; + } + + /// Extracts a window handle. VS is a 64 bit process but EnvDTE declares HWnd as a 32 bit int, + /// so depending on the marshalling we might see I4, UI4 or I8 (HWND values are 32 bit significant). + public Result GetHwnd() + { + switch (vt) + { + case .I4: + return (int)lVal; + case .UI4: + return (int)(uint32)lVal; + case .I8: + return (int)llVal; + default: + return .Err; + } + } +} + +[CRepr] +public struct DISPPARAMS +{ + public VARIANT* rgvarg; + public int32* rgdispidNamedArgs; + public uint32 cArgs; + public uint32 cNamedArgs; +} + +[CRepr] +public struct EXCEPINFO +{ + public uint16 wCode; + public uint16 wReserved; + public BSTR bstrSource; + public BSTR bstrDescription; + public BSTR bstrHelpFile; + public uint32 dwHelpContext; + public void* pvReserved; + public function [CallingConvention(.Stdcall)] HResult(EXCEPINFO* excepInfo) pfnDeferredFillIn; + public int32 scode; + + public void FreeStrings() mut + { + if (bstrSource != null) + SysFreeString(bstrSource); + if (bstrDescription != null) + SysFreeString(bstrDescription); + if (bstrHelpFile != null) + SysFreeString(bstrHelpFile); + + bstrSource = null; + bstrDescription = null; + bstrHelpFile = null; + } +} + +[CRepr] +public struct IDispatch : IUnknown +{ + public const new Guid IID = .(0x00020400, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + public new VTable* VT { get => (.)mVT; } + + public HResult GetIDsOfNames(Guid* riid, char16** names, uint32 nameCount, uint32 localeId, int32* dispIds) mut => + VT.GetIDsOfNames(&this, riid, names, nameCount, localeId, dispIds); + public HResult Invoke(int32 dispIdMember, Guid* riid, uint32 localeId, DispatchFlags flags, DISPPARAMS* dispParams, VARIANT* result, EXCEPINFO* excepInfo, uint32* argErr) mut => + VT.Invoke(&this, dispIdMember, riid, localeId, flags, dispParams, result, excepInfo, argErr); + + [CRepr] + public struct VTable : IUnknown.VTable + { + // Slots we don't use, kept as placeholders so the layout stays correct. + public void* GetTypeInfoCount; + public void* GetTypeInfo; + public function [CallingConvention(.Stdcall)] HResult(IDispatch* self, Guid* riid, char16** names, uint32 nameCount, uint32 localeId, int32* dispIds) GetIDsOfNames; + public function [CallingConvention(.Stdcall)] HResult(IDispatch* self, int32 dispIdMember, Guid* riid, uint32 localeId, DispatchFlags flags, DISPPARAMS* dispParams, VARIANT* result, EXCEPINFO* excepInfo, uint32* argErr) Invoke; + } +} + +namespace DirectX.Common +{ + extension HResult + { + /// The COM server rejected the call (e.g. Visual Studio is busy). Retry later. + public const HResult RPC_E_CALL_REJECTED = (.)0x80010001; + /// The COM server asked us to retry the call later. + public const HResult RPC_E_SERVERCALL_RETRYLATER = (.)0x8001010A; + /// IDispatch::Invoke failed with an exception, details are in the EXCEPINFO. + public const HResult DISP_E_EXCEPTION = (.)0x80020009; + } +} + +#endif diff --git a/GlitchyEditor/src/Platform/Windows/Com/RunningObjectTable.bf b/GlitchyEditor/src/Platform/Windows/Com/RunningObjectTable.bf new file mode 100644 index 0000000..b161a07 --- /dev/null +++ b/GlitchyEditor/src/Platform/Windows/Com/RunningObjectTable.bf @@ -0,0 +1,107 @@ +#if BF_PLATFORM_WINDOWS + +using System; +using DirectX.Common; + +namespace GlitchyEditor.Platform.Windows.Com; + +static +{ + [Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern HResult GetRunningObjectTable(uint32 reserved, out IRunningObjectTable* runningObjectTable); + [Import("Ole32.lib"), CLink, CallingConvention(.Stdcall)] + public static extern HResult CreateBindCtx(uint32 reserved, out IBindCtx* bindCtx); +} + +/// Opaque, only ever passed along as a pointer (e.g. to IMoniker.GetDisplayName). +[CRepr] +public struct IBindCtx : IUnknown +{ + public const new Guid IID = .(0x0000000e, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); +} + +[CRepr] +public struct IMoniker : IUnknown +{ + public const new Guid IID = .(0x0000000f, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + public new VTable* VT { get => (.)mVT; } + + /// The returned display name must be freed with CoTaskMemFree. + public HResult GetDisplayName(IBindCtx* bindCtx, IMoniker* monikerToLeft, out char16* displayName) mut => + VT.GetDisplayName(&this, bindCtx, monikerToLeft, out displayName); + + [CRepr] + public struct VTable : IUnknown.VTable + { + // IMoniker inherits IPersistStream (which inherits IPersist). We only need GetDisplayName, + // all other slots are placeholders that keep the layout correct. + public void* GetClassID; // IPersist + public void* IsDirty; // IPersistStream + public void* Load; + public void* Save; + public void* GetSizeMax; + public void* BindToObject; // IMoniker + public void* BindToStorage; + public void* Reduce; + public void* ComposeWith; + public void* Enum; + public void* IsEqual; + public void* Hash; + public void* IsRunning; + public void* GetTimeOfLastChange; + public void* Inverse; + public void* CommonPrefixWith; + public void* RelativePathTo; + public function [CallingConvention(.Stdcall)] HResult(IMoniker* self, IBindCtx* bindCtx, IMoniker* monikerToLeft, out char16* displayName) GetDisplayName; + // ParseDisplayName and IsSystemMoniker follow but are never called. + } +} + +[CRepr] +public struct IEnumMoniker : IUnknown +{ + public const new Guid IID = .(0x00000102, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + public new VTable* VT { get => (.)mVT; } + + /// Returns S_OK if an element was fetched, S_FALSE if the enumeration is exhausted. + public HResult Next(uint32 count, out IMoniker* monikers, out uint32 fetchedCount) mut => + VT.Next(&this, count, out monikers, out fetchedCount); + + [CRepr] + public struct VTable : IUnknown.VTable + { + public function [CallingConvention(.Stdcall)] HResult(IEnumMoniker* self, uint32 count, out IMoniker* monikers, out uint32 fetchedCount) Next; + public void* Skip; + public void* Reset; + public void* Clone; + } +} + +[CRepr] +public struct IRunningObjectTable : IUnknown +{ + public const new Guid IID = .(0x00000010, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + public new VTable* VT { get => (.)mVT; } + + public HResult GetObject(IMoniker* objectName, out IUnknown* outObject) mut => + VT.GetObject(&this, objectName, out outObject); + public HResult EnumRunning(out IEnumMoniker* enumMoniker) mut => + VT.EnumRunning(&this, out enumMoniker); + + [CRepr] + public struct VTable : IUnknown.VTable + { + public void* Register; + public void* Revoke; + public void* IsRunning; + public function [CallingConvention(.Stdcall)] HResult(IRunningObjectTable* self, IMoniker* objectName, out IUnknown* outObject) GetObject; + public void* NoteChangeTime; + public void* GetTimeOfLastChange; + public function [CallingConvention(.Stdcall)] HResult(IRunningObjectTable* self, out IEnumMoniker* enumMoniker) EnumRunning; + } +} + +#endif diff --git a/GlitchyEngine/vendor/directx b/GlitchyEngine/vendor/directx index 65f5bbb..6de8938 160000 --- a/GlitchyEngine/vendor/directx +++ b/GlitchyEngine/vendor/directx @@ -1 +1 @@ -Subproject commit 65f5bbbf01483f8c7699c8a1043cbbe6d6e16d9c +Subproject commit 6de893819088874dd2b1a455f058ae3b7540012e