都是用 DllImport?有沒有考慮過自己寫一個 extern 方法?

都是用 DllImport?有沒有考慮過自己寫一個 extern 方法?

發佈於 2018-09-06 13:58 更新於 2018-09-06 15:04

你做 .NET 開發的時候,一定用過 DllImport 這個特性吧,這貨是用於 P/Invoke (Platform Invoke, 平臺調用) 的。這種 DllImport 標記的方法都帶有一個 extern 關鍵字。

那麼有沒有可能我們自己寫一個自己的 extern 方法呢?答案是可以的。本文就寫一個這樣的例子。


DllImport

日常我們的平臺調用代碼是這樣的:

class Walterlv
{
    [STAThread]
    static void Main(string[] args)
    {
        var hwnd = FindWindow(null, "那個窗口的標題欄文字");
        // 此部分代碼省略。
    }

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}

你看不到 FindWindow 的實現。

自定義的 extern

那我們能否自己實現一個這樣的 extern 的方法呢?寫一寫,還真是能寫得出來的。

▲ 外部方法需要 Attribute 的提示

只不過如果你裝了 ReSharper,會給出一個提示,告訴你外部方法應該寫一個 Attribute 在上面(雖然實際上編譯沒什麼問題)。

那麼我們就真的寫一個 Attribute 在上面吧。

class Walterlv
{
    internal void Run()
    {
        Foo();
    }

    [WalterlvHiddenMethod]
    private static extern void Foo();
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class WalterlvHiddenMethodAttribute : Attribute
{
}

如果你好奇如果沒寫 Attribute 會怎樣,那我可以告訴你 —— 你寫不寫都一樣,都是不能運行起來的。

▲ 方法沒有實現

讓自定義的 extern 工作起來

如果無法運行,那麼我們寫 extern 是完全沒有意義的。於是我們怎麼能讓這個“外部的”函數工作起來呢?—— 事實上就是工作不起來。

不過,我們能夠控制編譯過程,能夠在編譯期間爲其添加一個實現。

這裏,我們需要用到 MSBuild/Roslyn 相關的知識:

當你讀完上面那篇文章,你就明白我想幹啥了。沒錯,在編譯期間將其替換成一個擁有實現的函數。

現在,我們將我們的幾個類放到不同的文件中。

▲ 我們的項目文件

// Program.cs
class Walterlv
{
    [STAThread]
    static void Main(string[] args)
    {
        Demo.Foo();
    }
}
// Demo.cs
class Demo
{
    [WalterlvHiddenMethod]
    internal static extern void Foo();
}
// WalterlvHiddenMethodAttribute.cs
using System;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class WalterlvHiddenMethodAttribute : Attribute
{
}

No!我們還有一個隱藏文件 Demo.implemented.cs

▲ 隱藏的文件

// Demo.implemented.cs
using System;

class Demo
{
    internal static void Foo()
    {
        Console.WriteLine("我就是一個外部方法。");
    }
}

這個文件我是通過在 csproj 中將其 remove 掉使得在解決方案中看不見。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Demo.implemented.cs" />
  </ItemGroup>

</Project>

然後,我們按照上文博客中所說的方式,添加一個 Target,在編譯時替換這個文件:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net472</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Demo.implemented.cs" />
  </ItemGroup>

  <Target Name="WalterlvReplaceMethod" BeforeTargets="BeforeBuild">
    <ItemGroup>
      <Compile Remove="Demo.cs" Visible="false" />
      <Compile Include="Demo.implemented.cs" Visible="false" />
    </ItemGroup>
  </Target>

</Project>

現在,運行即會發現可以運行。

▲ 可以運行

總結

  • extern 是 C# 的一個語法而已,誰都可以用,但最終編譯時的 C# 文件必須都有實現。
  • 我們可以在編譯時修改編譯的文件來爲這些未實現的方法添加實現。

原理

看完上面的方法,是不是覺得寫一個把實現藏起來的 extern 方法很簡單?

但如果你認爲 DllImport 也是這麼做的那就不對了。

還記得我們一開始寫的 FindWindow 方法嗎?我們查看其編譯後的 IL 代碼,可以發現其外部調用已經寫到了 IL 裏面了,並且其實現使用了 pinvokeimpl 關鍵字。也就是說,具體的調用是 JIT 編譯器去做的事兒。

.method public hidebysig static pinvokeimpl ( "user32.dll" unicode winapi )native int 
    FindWindow(
      string lpClassName, 
      string lpWindowName
    ) cil managed preservesig 
{
    // Can't find a body
} // end of method Walterlv::FindWindow

至於實際執行時的執行細節,可以閱讀 c# - How does DllImport really work? - Stack Overflow 瞭解更多。

如果去看看我們寫的 Foo 的 IL,就完全不一樣了:

.method assembly hidebysig static void 
    Foo() cil managed 
{
    .custom instance void WalterlvHiddenMethodAttribute::.ctor() 
      = (01 00 00 00 )
    .maxstack 8

    IL_0000: nop          
    IL_0001: ldstr        "我就是一個外部方法。"
    IL_0006: call         void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop          
    IL_000c: ret          

} // end of method Demo::Foo

這其實就是我們在 Demo.implement.cs 中寫的那個函數的實現。這是當然,畢竟我們編譯時偷偷把這個函數換成了那個隱藏的文件實現了。

關於如何迅速查看 C# 代碼對應的 IL,可以閱讀我的另一篇博客:如何快速編寫和調試 Emit 生成 IL 的代碼


參考資料

本文會經常更新,請閱讀原文: https://walterlv.com/post/write-your-own-extern-method.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含鏈接: https://walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請 與我聯繫 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章