松耦合 NativeMethods

发布于 2024-12-17 02:09:26 字数 1591 浏览 1 评论 0原文

我需要使用 C# 的本机 DLL。该 DLL 公开了几种我可以通过 P/Invoke 访问的方法和多种类型。所有这些代码都位于标准 NativeMethods 类中。为了简单起见,它看起来像这样:

internal static class NativeMethods
{
    [DllImport("Foo.dll", SetLastError = true)]
    internal static extern ErrorCode Bar(ref Baz baz);

    internal enum ErrorCode { None, Foo, Baz,... } 

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct Baz
    {
        public int Foo;
        public string Bar;
    }
}

为了实现松散耦合,我需要提取一个接口并使用对 NativeMethods 的调用来实现它:

public interface IFoo
{
    void Bar(ref Baz baz);
}

public class Foo : IFoo
{
    public void Bar(ref Baz baz)
    {
         var errorCode = NativeMethods.Bar(baz);
         if (errorCode != ErrorCode.None) throw new FooException(errorCode);
    }         
}

现在我可以在代码中使用 IFoo 作为依赖项并在测试中模拟它:

public class Component : IComponent
{
     public Component(IFoo foo, IService service) { ... }
}

这里似乎有问题。根据 FxCop,NativeMethods 必须是内部的。那么 IFoo (从 NativeMethods 中提取)也是内部的,这是有道理的。但我不能只是将其设置为内部 b/c 它在公共 ctor 中使用(应该保持公共)。所以:为了实现松散耦合,我必须更改组件的可见性,否则该组件将是内部组件。你们对此有何看法?

另一个问题:组件具有方法 public void DoSomehining(Bar bar),它使用 NativeMethods.cs 中定义的 Bar。我也必须将其公开才能使其发挥作用。或者创建一个新的 Bar 类来包装 NativeMethods+Bar 。如果我采用公共方式,那么 NativeMethods 也会变得公共,并且 FxCop 抱怨“嵌套类型不应该可见”。如果我采用包装方式......好吧,我觉得为所有“本地类型”做这件事太费力了。哦,还有第三种方法:将类型从 NativeMethods 中移走并将其公开。然后,FxCop 开始分析它们,并找到嵌套在 NativeMethods 中时隐藏的所有错误。我真的不知道这里最好的方法是什么......

I need to use a native DLL from C#. The DLL exposes several methods which I can access via P/Invoke, and several types. All this code is in a standard NativeMethods class. To keep things simple it looks like this:

internal static class NativeMethods
{
    [DllImport("Foo.dll", SetLastError = true)]
    internal static extern ErrorCode Bar(ref Baz baz);

    internal enum ErrorCode { None, Foo, Baz,... } 

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct Baz
    {
        public int Foo;
        public string Bar;
    }
}

To achieve loose coupling I need to extract an interface and implement it using calls to NativeMethods:

public interface IFoo
{
    void Bar(ref Baz baz);
}

public class Foo : IFoo
{
    public void Bar(ref Baz baz)
    {
         var errorCode = NativeMethods.Bar(baz);
         if (errorCode != ErrorCode.None) throw new FooException(errorCode);
    }         
}

Now I can just use IFoo as a dependency in my code and mock it in tests:

public class Component : IComponent
{
     public Component(IFoo foo, IService service) { ... }
}

Something seems wrong here. NativeMethods must be internal according to FxCop. It makes sense then for IFoo (which was extracted from NativeMethods) to be internal too. But I can't just make it internal b/c it is used in a public ctor (which should stay public). So: in order to achieve loose coupling I have to change the visibility of a component which otherwise would have been internal. What do you guys think about this?

Another problem: Component has method public void DoSomehing(Bar bar) which uses the Bar defined in NativeMethods.cs. I have to make that public too for this to work. This, or make a new Bar class which wraps NativeMethods+Bar. If I go the public way then NativeMethods becomes public too and FxCop complains that "Nested types should not be visible". If I go the wrapper way... well, I feel it's too much of an effort to do it for all the "native types". Oh, there is the 3rd way: move the types away from NativeMethods and make them public. Then FxCop gets to analyze them and finds all the errors which were otherwise hidden when they were nested in NativeMethods. I really don't know what's the best way here...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

挽心 2024-12-24 02:09:26

公共抽象类(而不是接口)可能是您的朋友。

这可以包括内部抽象方法(指的是内部类型),这实际上使得不可能以正常方式从程序集外部进行子类化(但是InternalsVisibleTo将允许您创建一个假的用于测试)。

基本上,接口的设计并没有真正按照组件方面的方式进行设计。

这正是我在 Noda Time 中为 CalendarSystem 所做的 - 它的 API 使用内部类型,但我想让它成为一个接口或抽象类。我在 博客文章,您可能会觉得有趣。

It's possible that a public abstract class is your friend here, instead of an interface.

That can include internal abstract methods (referring to internal types), which actually makes it impossible to subclass in the normal way from outside the assembly (but InternalsVisibleTo will let you create a fake for testing).

Basically interfaces haven't really been designed as well as they could have been from a component aspect.

This is exactly what I did in Noda Time for CalendarSystem - its API uses internal types, but I wanted to make it an interface or abstract class. I wrote about the oddities of access in a blog post which you may find interesting.

看春风乍起 2024-12-24 02:09:26

提取INativeMethods怎么样?

我们将免费获得的不完整列表:

  • 对TDD的完整支持
  • 您不需要运行应用程序来验证大多数情况
  • 真的很容易模拟并添加对不同环境\操作系统
  • 分析性能的支持,测量对 WinApi 的调用计数 向

我展示代码

接口与 WinApi 相同:

 internal interface INativeMethods    
 {
    IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam);

    bool GetWindowRect(IntPtr hwnd, out Rect rect);

    IntPtr GetWindow(IntPtr hwnd, uint cmd);

    bool IsWindowVisible(IntPtr hwnd);

    long GetTickCount64();

    int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount);

    int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect);

    bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement);

    int GetDeviceCaps(IntPtr hdc, int index);
}

实现是静态类的瘦代理:

internal class NativeMethodsWraper : INativeMethods
{       
    public IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam)
    {
        return NativeMethods.SendMessage(hwnd, msg, wparam, lparam);
    }

    public bool GetWindowRect(IntPtr hwnd, out Rect rect)
    {
        return NativeMethods.GetWindowRect(hwnd, out rect);
    }

    public IntPtr GetWindow(IntPtr hwnd, uint cmd)
    {
        return NativeMethods.GetWindow(hwnd, cmd);
    }

    public bool IsWindowVisible(IntPtr hwnd)
    {
        return NativeMethods.IsWindowVisible(hwnd);
    }

    public long GetTickCount64()
    {
        return NativeMethods.GetTickCount64();
    }

    public int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount)
    {
        return NativeMethods.GetClassName(hwnd, classNameBuffer, maxCount);
    }

    public int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect)
    {
        return NativeMethods.DwmGetWindowAttribute(hwnd, attribute, out rect, sizeOfRect);
    }

    public bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement)
    {
        return NativeMethods.GetWindowPlacement(hwnd, ref pointerToWindowPlacement);
    }

    public int GetDeviceCaps(IntPtr hdc, int index)
    {
        return NativeMethods.GetDeviceCaps(hdc, index);
    }
}

让我们使用 P\Invoke 导入来完成此

internal static class NativeMethods
{
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
    public static extern int GetDeviceCaps(IntPtr hdc, int index);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, [Out] StringBuilder lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("kernel32.dll")]
    public static extern long GetTickCount64();

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);

    [DllImport(@"dwmapi.dll")]
    public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);
}

操作 示例用法

internal class GetTickCount64TimeProvider : ITimeProvider
{
    private readonly INativeMethods _nativeMethods;

    public GetTickCount64TimeProvider(INativeMethods nativeMethods)
    {
        _nativeMethods = nativeMethods;
    }

    public Timestamp Now()
    {
        var gtc = _nativeMethods.GetTickCount64();

        var getTickCountStamp = Timestamp.FromMilliseconds(gtc);
        return getTickCountStamp;
    }
}

单元测试

很难相信,但有可能通过模拟 WinApi 来验证任何期望

[Test]
public void GetTickCount64_ShouldCall_NativeMethod()
{
    var nativeMock = MockRepository.GenerateMock<INativeMethods>();            
    var target = GetTarget(nativeMock);

    target.Now();

    nativeMock.AssertWasCalled(_ => _.GetTickCount64());
}



[Test]
public void Now_ShouldReturn_Microseconds()
{
    var expected = Timestamp.FromMicroseconds((long) int.MaxValue * 1000);
    var nativeStub = MockRepository.GenerateStub<INativeMethods>();
    nativeStub.Stub(_ => _.GetTickCount64()).Return(int.MaxValue);
    var target = GetTarget(nativeStub);

    var actual = target.Now();

    Assert.AreEqual(expected, actual);
}


private static GetTickCount64TimeProvider GetTarget(INativeMethods nativeMock)
{
    return new GetTickCount64TimeProvider(nativeMock);
}

模拟 out\ref 参数可能会引起头痛,因此这里是供将来参考的代码:

[Test]
public void When_WindowIsMaximized_PaddingBordersShouldBeExcludedFromArea()
{
    // Top, Left are -8 when window is maximized but should be 0,0
    // http://blogs.msdn.com/b/oldnewthing/archive/2012/03/26/10287385.aspx
    INativeMethods nativeMock = MockRepository.GenerateStub<INativeMethods>();

    var windowRectangle = new Rect() {Left = -8, Top = -8, Bottom = 1216, Right = 1936};
    var expectedScreenBounds = new Rect() {Left = 0, Top = 0, Bottom = 1200, Right = 1920};
    _displayInfo.Stub(_ => _.GetScreenBoundsFromWindow(windowRectangle.ToRectangle())).Return(expectedScreenBounds.ToRectangle());

    var hwnd = RandomNativeHandle();
    StubForMaximizedWindowState(nativeMock, hwnd);
    StubForDwmRectangle(nativeMock, hwnd, windowRectangle);
    WindowCoverageReader target = GetTarget(nativeMock);

    var window = target.GetWindowFromHandle(hwnd);


    Assert.AreEqual(WindowState.Maximized, window.WindowState);
    Assert.AreEqual(expectedScreenBounds.ToRectangle(), window.Area);
}

private void StubForDwmRectangle(INativeMethods nativeMock, IntPtr hwnd, Rect rectToReturnFromWinApi)
{
    var sizeOf = Marshal.SizeOf(rect);
    var rect = new Rect();

    nativeMock.Stub(_ =>
    {
        _.DwmGetWindowAttribute(
            hwnd, 
            (int)DwmWindowAttribute.DwmwaExtendedFrameBounds,
            out rect,  // called with zeroed object
            sizeOf);
    }).OutRef(rectToReturnFromWinApi).Return(0);
}

private IntPtr RandomNativeHandle()
{
    return new IntPtr(_random.Next());
}

private void StubForMaximizedWindowState(INativeMethods nativeMock, IntPtr hwnd)
{
    var maximizedFlag = 3;
    WindowPlacement pointerToWindowPlacement = new WindowPlacement() {ShowCmd = maximizedFlag};
    nativeMock.Stub(_ => { _.GetWindowPlacement(Arg<IntPtr>.Is.Equal(hwnd), ref Arg<WindowPlacement>.Ref(new Anything(), pointerToWindowPlacement).Dummy); }).Return(true);
}

How about extracting INativeMethods?

Incomplete list of what we will get for free:

  • Complete support for TDD
  • You do not need to run your application to verify most of cases
  • Really easy to simulate and add support for different environments\OS
  • Profiling performance, measure call count to WinApi

Show me the code

Interface is identical to WinApi:

 internal interface INativeMethods    
 {
    IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam);

    bool GetWindowRect(IntPtr hwnd, out Rect rect);

    IntPtr GetWindow(IntPtr hwnd, uint cmd);

    bool IsWindowVisible(IntPtr hwnd);

    long GetTickCount64();

    int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount);

    int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect);

    bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement);

    int GetDeviceCaps(IntPtr hdc, int index);
}

Implementation is a thin proxy to static class:

internal class NativeMethodsWraper : INativeMethods
{       
    public IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam)
    {
        return NativeMethods.SendMessage(hwnd, msg, wparam, lparam);
    }

    public bool GetWindowRect(IntPtr hwnd, out Rect rect)
    {
        return NativeMethods.GetWindowRect(hwnd, out rect);
    }

    public IntPtr GetWindow(IntPtr hwnd, uint cmd)
    {
        return NativeMethods.GetWindow(hwnd, cmd);
    }

    public bool IsWindowVisible(IntPtr hwnd)
    {
        return NativeMethods.IsWindowVisible(hwnd);
    }

    public long GetTickCount64()
    {
        return NativeMethods.GetTickCount64();
    }

    public int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount)
    {
        return NativeMethods.GetClassName(hwnd, classNameBuffer, maxCount);
    }

    public int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect)
    {
        return NativeMethods.DwmGetWindowAttribute(hwnd, attribute, out rect, sizeOfRect);
    }

    public bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement)
    {
        return NativeMethods.GetWindowPlacement(hwnd, ref pointerToWindowPlacement);
    }

    public int GetDeviceCaps(IntPtr hdc, int index)
    {
        return NativeMethods.GetDeviceCaps(hdc, index);
    }
}

Let's complete this with P\Invoke imports

internal static class NativeMethods
{
    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
    public static extern int GetDeviceCaps(IntPtr hdc, int index);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, [Out] StringBuilder lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("kernel32.dll")]
    public static extern long GetTickCount64();

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);

    [DllImport(@"dwmapi.dll")]
    public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);
}

Example usage

internal class GetTickCount64TimeProvider : ITimeProvider
{
    private readonly INativeMethods _nativeMethods;

    public GetTickCount64TimeProvider(INativeMethods nativeMethods)
    {
        _nativeMethods = nativeMethods;
    }

    public Timestamp Now()
    {
        var gtc = _nativeMethods.GetTickCount64();

        var getTickCountStamp = Timestamp.FromMilliseconds(gtc);
        return getTickCountStamp;
    }
}

Unit testing

Hard to believe, but it possible to verify any expectation by mocking WinApi

[Test]
public void GetTickCount64_ShouldCall_NativeMethod()
{
    var nativeMock = MockRepository.GenerateMock<INativeMethods>();            
    var target = GetTarget(nativeMock);

    target.Now();

    nativeMock.AssertWasCalled(_ => _.GetTickCount64());
}



[Test]
public void Now_ShouldReturn_Microseconds()
{
    var expected = Timestamp.FromMicroseconds((long) int.MaxValue * 1000);
    var nativeStub = MockRepository.GenerateStub<INativeMethods>();
    nativeStub.Stub(_ => _.GetTickCount64()).Return(int.MaxValue);
    var target = GetTarget(nativeStub);

    var actual = target.Now();

    Assert.AreEqual(expected, actual);
}


private static GetTickCount64TimeProvider GetTarget(INativeMethods nativeMock)
{
    return new GetTickCount64TimeProvider(nativeMock);
}

Mocking out\ref params might cause headache so here is the code for future reference:

[Test]
public void When_WindowIsMaximized_PaddingBordersShouldBeExcludedFromArea()
{
    // Top, Left are -8 when window is maximized but should be 0,0
    // http://blogs.msdn.com/b/oldnewthing/archive/2012/03/26/10287385.aspx
    INativeMethods nativeMock = MockRepository.GenerateStub<INativeMethods>();

    var windowRectangle = new Rect() {Left = -8, Top = -8, Bottom = 1216, Right = 1936};
    var expectedScreenBounds = new Rect() {Left = 0, Top = 0, Bottom = 1200, Right = 1920};
    _displayInfo.Stub(_ => _.GetScreenBoundsFromWindow(windowRectangle.ToRectangle())).Return(expectedScreenBounds.ToRectangle());

    var hwnd = RandomNativeHandle();
    StubForMaximizedWindowState(nativeMock, hwnd);
    StubForDwmRectangle(nativeMock, hwnd, windowRectangle);
    WindowCoverageReader target = GetTarget(nativeMock);

    var window = target.GetWindowFromHandle(hwnd);


    Assert.AreEqual(WindowState.Maximized, window.WindowState);
    Assert.AreEqual(expectedScreenBounds.ToRectangle(), window.Area);
}

private void StubForDwmRectangle(INativeMethods nativeMock, IntPtr hwnd, Rect rectToReturnFromWinApi)
{
    var sizeOf = Marshal.SizeOf(rect);
    var rect = new Rect();

    nativeMock.Stub(_ =>
    {
        _.DwmGetWindowAttribute(
            hwnd, 
            (int)DwmWindowAttribute.DwmwaExtendedFrameBounds,
            out rect,  // called with zeroed object
            sizeOf);
    }).OutRef(rectToReturnFromWinApi).Return(0);
}

private IntPtr RandomNativeHandle()
{
    return new IntPtr(_random.Next());
}

private void StubForMaximizedWindowState(INativeMethods nativeMock, IntPtr hwnd)
{
    var maximizedFlag = 3;
    WindowPlacement pointerToWindowPlacement = new WindowPlacement() {ShowCmd = maximizedFlag};
    nativeMock.Stub(_ => { _.GetWindowPlacement(Arg<IntPtr>.Is.Equal(hwnd), ref Arg<WindowPlacement>.Ref(new Anything(), pointerToWindowPlacement).Dummy); }).Return(true);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文