都是用 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]) 。