当前位置: 首页 > news >正文

都是用 DllImport?有没有考虑过自己写一个 extern 方法?

原文 都是用 DllImport?有没有考虑过自己写一个 extern 方法?

你做 .NET 开发的时候,一定用过 DllImport 这个特性吧,这货是用于 P/Invoke (Platform Invoke, 平台调用) 的。这种 DllImport 标记的方法都带有一个 extern 关键字。

那么有没有可能我们自己写一个自己的 extern 方法呢?答案是可以的。本文就写一个这样的例子。


本文内容
  • 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 的提示
▲ 外部方法需要 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 相关的知识:

  • Roslyn 通过 Target 修改编译的文件 - 林德熙

当你读完上面那篇文章,你就明白我想干啥了。没错,在编译期间将其替换成一个拥有实现的函数。

现在,我们将我们的几个类放到不同的文件中。

我们的项目文件
▲ 我们的项目文件

// 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 的代码。


参考资料

  • c# - How does DllImport really work? - Stack Overflow

本文会经常更新,请阅读原文: https://walterlv.com/post/write-your-own-extern-method.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

相关文章:

  • python实现将json数据以json格式写入txt文件
  • 【笔记】Nginx热更新相关知识
  • 简单读!spring-mvc源码之穿越http请求
  • C++与Rust操作裸指针的比较
  • 团队项目的NABCD的分析
  • .net core 依赖注入的基本用发
  • win10驱动下获取cpu信息
  • 闭包--闭包之tab栏切换(四)
  • 单据类报表的制作
  • Spring Boot(1)
  • mac终端常用命令
  • ELK 使用小技巧(第 2 期)
  • Workbook导出excel封装的工具类
  • 勒索病毒防范方案-有韩立刚老师总结的非常规手段
  • js导出excel文件
  • [译]CSS 居中(Center)方法大合集
  • 【Under-the-hood-ReactJS-Part0】React源码解读
  • 2017 前端面试准备 - 收藏集 - 掘金
  • Android优雅地处理按钮重复点击
  • Java 最常见的 200+ 面试题:面试必备
  • k个最大的数及变种小结
  • React 快速上手 - 06 容器组件、展示组件、操作组件
  • SSH 免密登录
  • Swoft 源码剖析 - 代码自动更新机制
  • Webpack4 学习笔记 - 01:webpack的安装和简单配置
  • 复杂数据处理
  • 构造函数(constructor)与原型链(prototype)关系
  • 机器学习 vs. 深度学习
  • 基于web的全景—— Pannellum小试
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • #{}和${}的区别?
  • #pragam once 和 #ifndef 预编译头
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (zt)基于Facebook和Flash平台的应用架构解析
  • (二)WCF的Binding模型
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真
  • (十六)一篇文章学会Java的常用API
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • .net Application的目录
  • :=
  • :not(:first-child)和:not(:last-child)的用法
  • [20150707]外部表与rowid.txt
  • [c++] 自写 MyString 类
  • [DP 训练] Longest Run on a Snowboard, UVa 10285
  • [IDF]聪明的小羊
  • [Interview]Java 面试宝典系列之 Java 多线程
  • [iOS开发]iOS中TabBar中间按钮凸起的实现
  • [JS真好玩] 掘金创作者必备: 监控每天是谁取关了你?
  • [LeetCode]284. Peeking Iterator(C++,类,暴力)
  • [Linux] day07——查看及过滤文本
  • [NOIP2005]过河
  • [NOIP2011DAY1P1]铺地毯
  • [OLEDB] 目前还找找不到处理下面错误的办法