从 GUID 创建任意 Visual Studio COM 对象(例如 IDebugEngine2)

Create Arbitrary Visual Studio COM Object (such as IDebugEngine2) from GUID

我正在研究为 Visual Studio 目前不支持的语言开发一个新的项目系统。

此语言已经存在第三方调试器适配器协议服务器,但是由于它似乎无法与 Visual Studio 中使用的本机 DAP 客户端一起正常工作,我想编写自己的 AD7Engine并简单地将我不需要修改的所有调用推迟到 Microsoft.VisualStudio.Debugger.VSCodeDebuggerHost.dll

中的原始实现

理论上我觉得这个应该比较简单;给定目标 COM 对象

namespace Microsoft.VisualStudio.Debugger.VSCodeDebuggerHost.AD7.Implementation
{
  [ComVisible(true)]
  [Guid("8355452D-6D2F-41b0-89B8-BB2AA2529E94")]
  internal class AD7Engine

我想我应该可以做类似

的事情
var type = Type.GetTypeFromCLSID(new Guid("8355452D-6D2F-41b0-89B8-BB2AA2529E94"));
var instance = Activator.CreateInstance(type);

但是这失败了,说 class 没有注册。即使我尝试使用此技术创建我的 own AD7Engine 的实例,它也会显示 class 未注册。我觉得这个问题的症结在于 COM 对象是在 HKCU\Software\Microsoft\VisualStudio\<version>\CLSID 下的注册表中定义的(或者可能在 Visual Studio 的新版本中的私有注册表配置单元下),但是我强烈怀疑 Activator.CreateInstance 不知道那个位置,所以看不到如何创建它。

我一直在 IDA 中分离 vsdebug.dll 以查看引擎创建细节的来源;最终,它使用了与 this, this and this

非常相似的技术
v8 = GetLoader(a1, &rclsid);

if (v8 < 0)
    goto LABEL_26;
v8 = GetModulePath(a1, &bstrString);
if (v8 < 0)
{
    v4 = bstrString;
    goto LABEL_26;
}
v9 = CoCreateInstance(&rclsid, 0, dwClsContext, &_GUID_10e4254c_6d73_4c38_b011_e0049b2e0a0f, &ppv);
v4 = bstrString;
if (v9 < 0)
{
    v8 = -2147155456;
    goto LABEL_26;
}
v8 = (*(int(__stdcall * *)(LPVOID, BSTR, IID *, LPUNKNOWN, DWORD, LPVOID *))(*(_DWORD*)ppv + 28))(
    ppv,
    bstrString,
    a1,
    pUnkOuter,
    dwClsContext,
    v17);
if (v8 < 0)
{
    LABEL_26:
    v11 = CoCreateInstance(a1, pUnkOuter, dwClsContext, &IID_IUnknown, v17);
    if (v11 >= 0 || v8 != -2147155456)

但最终我觉得我不应该求助于尝试计算 VS CLSID 注册表路径(vsdebug.dll 来自某个未知的地方,其他人只是计算或硬编码);我觉得应该有一些高级方法可以在 Visual Studio 上下文中创建任意 COM 对象,类似于使用 ServiceProvider.GetService().

就我而言,这整件事是一个有趣的学习练习,所以这是否是个好主意与我无关;我致力于在这个阶段找到答案;我只需要知道

中存在什么 API

如有任何帮助,我们将不胜感激

经过更多研究,所有这些问题的答案...都是肯定的!

鉴于微软声明所有从 RegLoadAppKey 加载的私有应用程序配置单元必须使用打开配置单元时获得的原始句柄,我很好奇 Visual Studio 是如何实现这一点的,考虑到我在任何地方都看不到对此类句柄的任何引用,但是当我跨过 WinDbg 中的注册表函数时,Process Monitor 报告已访问私有配置单元。

进入这些函数后,我发现实际上 Visual Studio 绕过所有 Win32 注册表函数到它自己的特殊处理程序,该处理程序确定是否需要重定向函数调用。

因此,我们可以得出结论

  • 如果需要,任何访问 Visual Studio 注册表项的尝试都将自动重定向到 Visual Studio 的私有应用程序配置单元

这仍然给我们留下了必须首先构建根 Visual Studio 密钥的问题。事实证明,您可以使用 VSRegistry.RegistryRoot 方法获取对各种配置存储的引用(尽管实际上只支持用户和配置类型)

因此,我们可以通过

获取对用户配置密钥的引用
VSRegistry.RegistryRoot(__VsLocalRegistryType.RegType_Configuration);

这仍然留给我们实际构造目标 COM 对象的问题。碰巧,Visual Studio 有一个 API 吗:ILocalRegistry3!

如果您已经知道要创建的对象的 CLSID,则可以直接使用 CreateInstance 方法创建它。在我的例子中,从 Microsoft.VisualStudio.Debugger.VSCodeDebuggerHost.dll 反编译的 AD7Engine 上的 GuidAttribute 实际上与注册表中 AD7Metrics 项下定义的 CLSID 不匹配。因此,我认为将引擎 ID GUID 转换为 AD7Engine COM 对象的 CLSID,然后创建实例更安全。

使用此技术还可以轻松创建 Microsoft.VisualStudio.ProjectSystem.Debug.DebugEngines class.

中指定的调试器实例
private IDebugEngine2 VsCreateDebugEngine(Guid engineId)
{
    var config = VSRegistry.RegistryRoot(__VsLocalRegistryType.RegType_Configuration);

    var subKeyPath = $"AD7Metrics\Engine\{{{engineId}}}";

    var engineKey = config.OpenSubKey(subKeyPath);

    if (engineKey == null)
        throw new ArgumentException($"Could not find an AD7Engine for GUID '{engineId}'");

    var clsid = engineKey.GetValue("CLSID")?.ToString();

    if (clsid == null)
        throw new InvalidOperationException($"GUID '{engineId}' does not have a CLSID value");

    var obj = VsCreateComObject(new Guid(clsid));

    return (IDebugEngine2) obj;
}

private object VsCreateComObject(Guid guid)
{
    var localRegistry = (ILocalRegistry3)Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider.GetService(typeof(SLocalRegistry));

    Guid riid = VSConstants.IID_IUnknown;

    IntPtr ptr;

    var result = localRegistry.CreateInstance(
        guid,
        null,
        ref riid,
        (uint) Microsoft.VisualStudio.OLE.Interop.CLSCTX.CLSCTX_INPROC_SERVER,
        out ptr
    );

    if (result != VSConstants.S_OK)
        throw new ArgumentException($"Failed to retrieve GUID '{guid}', GUID may not exist");

    var obj = Marshal.GetObjectForIUnknown(ptr);

    return obj;
}

您可以使用以下帮助程序轻松枚举所有可用的引擎

[DebuggerDisplay("Name = {Name}, EngineID = {EngineID}, {CLSID} = {CLSID}")]
class AD7Info
{
    public string Name { get; set; }

    public Guid EngineID { get; set; }

    public Guid? CLSID { get; set; }
}

private List<AD7Info> GetAD7Infos()
{
    var config = VSRegistry.RegistryRoot(__VsLocalRegistryType.RegType_Configuration);

    var engineKey = config.OpenSubKey("AD7Metrics\Engine");

    return engineKey.GetSubKeyNames().Select(n =>
    {
        var entryKey = engineKey.OpenSubKey(n);

        var clsid = entryKey.GetValue("CLSID")?.ToString();

        return new AD7Info
        {
            Name = entryKey.GetValue("Name")?.ToString(),
            EngineID = new Guid(n),
            CLSID = clsid != null ? new Guid(clsid) : (Guid?) null
        };
    }).ToList();
}

示例:

//Retrieve the "managed only" debugging engine
var result = VsCreateDebugEngine(DebuggerEngines.ManagedOnlyEngine);

或者在我的情况下

//Retrieve the VSCodeDebuggerHost debug engine
var result = VsCreateDebugEngine(new Guid("2833D225-C477-4388-9353-544D168F6030"));

我在我机器上安装的所有 38 个引擎上测试了这个;他们中的大多数人都成功了,可能失败的人需要通过他们的 ProgramProvider 或其他东西来创建。

耶!