Слабая связь NativeMethods - PullRequest
       48

Слабая связь NativeMethods

3 голосов
/ 17 ноября 2011

Мне нужно использовать нативную DLL из C #. 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) { ... }
}

Что-то здесь не так. NativeMethods должен быть внутренним в соответствии с FxCop. Тогда имеет смысл, чтобы IFoo (который был извлечен из NativeMethods) тоже был внутренним. Но я не могу просто сделать его внутренним, потому что он используется в публичном ctor (который должен оставаться публичным). Итак: Чтобы добиться слабой связи, мне нужно изменить видимость компонента, который в противном случае был бы внутренним. Что вы, ребята, думаете об этом?

Другая проблема: у компонента есть метод public void DoSomehing(Bar bar), который использует Bar, определенный в NativeMethods.cs. Я должен обнародовать это слишком, чтобы это работало. Это или создайте новый класс Bar, который обернет NativeMethods+Bar. Если я пойду публично, то NativeMethods тоже станет публичным, и FxCop жалуется, что «Вложенные типы не должны быть видны». Если я иду по пути обертки ... ну, я чувствую, что это слишком много усилий, чтобы сделать это для всех "нативных типов" О, есть третий способ: убрать типы из NativeMethods и сделать их общедоступными. Затем FxCop анализирует их и находит все ошибки, которые в противном случае были скрыты, когда они были вложены в NativeMethods. Я действительно не знаю, как лучше здесь ...

Ответы [ 2 ]

3 голосов
/ 17 ноября 2011

Возможно, здесь вам нужен общедоступный абстрактный класс, а не интерфейс.

Это может включать внутренние абстрактные методы (ссылающиеся на внутренние типы), что фактически делает это невозможнымдля создания подкласса обычным способом извне сборки (но InternalsVisibleTo позволит вам создать фальшивку для тестирования).

В принципе, интерфейсы на самом деле не были разработаны так хорошо, как могли быбыли из аспекта компонента.

Это именно то, что я сделал в Noda Time для CalendarSystem - его API использует внутренние типы, но я хотел сделать его интерфейсом или абстрактным классом,Я написал о странностях доступа в сообщении в блоге , которое может показаться вам интересным.

0 голосов
/ 26 сентября 2015

Как насчет извлечения 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);
}
...