你做 .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 的代码


参考资料

都是用 DllImport?有没有考虑过自己写一个 extern 方法?的更多相关文章

  1. 写一个 sum方法,在使用下面任一语法调用时,都可以正常工作

    console.log(sum(2,3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5 (至少)有两种方法可以做到: 方法1: functio ...

  2. Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。

    #29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...

  3. mysql 的 help 命令:每个命令,都有相应的反斜杠(\)加一个字母或字符的简写

    mysql> help For information about MySQL products and services, visit: http://www.mysql.com/ For d ...

  4. 获取器操作都是针对数据而不是数据集的,要通过append()方法添加数据表不存在的字段

    获取器操作都是针对数据而不是数据集的,要通过append()方法添加数据表不存在的字段 public function getMembership(){ //加入会员s_id = 1 $busines ...

  5. Java基础面试操作题: 线程问题,写一个死锁(原理:只有互相都等待对方放弃资源才会产生死锁)

    package com.swift; public class DeadLock implements Runnable { private boolean flag; DeadLock(boolea ...

  6. 编写自定义PE结构的程序(如何手写一个PE,高级编译器都是编译好的PE头部,例如MASM,TASM等,NASM,FASM是低级编译器.可以自定义结构)

    正在学PE结构...感谢个位大哥的文章和资料...这里先说声谢谢 一般高级编译器都是编译好的PE头部,例如MASM,TASM等一直都说NASM,FASM是低级编译器.可以自定义结构但是苦于无人发布相关 ...

  7. Java项目中每一个类都可以有一个main方法

    Java项目中每一个类都可以有一个main方法,但只有一个main方法会被执行,其他main方法可以对类进行单元测试. public class StaticTest { public static ...

  8. 都别说工资低了,我们来一起写简单的dom选择器吧!

    前言 我师父(http://www.cnblogs.com/aaronjs/)说应当阅读框架(jquery),所以老夫就准备开始看了 然后公司的师兄原来写了个dom选择器,感觉不错啊!!!原来自己从来 ...

  9. 编写一段程序,从标准输入读取string对象的序列直到连续出现两个相同的单词或者所有单词都读完为止。使用while循环一次读取一个单词,当一个单词连续出现两次是使用break语句终止循环。输出连续重复出现的单词,或者输出一个消息说明没有人任何单词是重复出现的。

    // test14.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #include< ...

随机推荐

  1. 4.2 Routing -- Defining Your Routes

    一.概述 1. 当应用程序启动时,路由器负责显示模板,加载数据,另外还设置应用程序的状态.这是通过匹配当前URL到你定义的routes来实现的. 2. Ember app router中的Map方法可 ...

  2. Java backup

    待补充 ........ 0:常用头文件(待补充) import java.util.Arrays; import java.util.HashSet; import java.util.TreeSe ...

  3. 文件名含中文的JavaWeb文件下载

    在javaweb项目中实现文件下载,当文件名中包含中文文字时,需要进行如下的处理,才能在浏览器端正常显示中文文件名: response.setContentType("octets/stre ...

  4. Fiddler 手机无法上网问题

    一.Fiddler版本升级后需要升级netframework 二.每一个域名点击需要授权 出现此页面依次点击显示详细信息和访问此网站. 有时候网站白板,可能是cdn域名没有授权的原因,可以直接在url ...

  5. 【软件位置】Linux查看软件安装的位置

    如果我们在Linux 系统上安装了某个软件,我们可以通过如下的三种方式来确定. 一.        Which 命令 Shell 的which 命令可以找出相关命令是否已经在搜索路径中. 如: [ro ...

  6. 请问使用jmeter在tcp取样器测试中服务器名称或ip,端口可以填变量值吗?

    请问使用jmeter在tcp取样器测试中服务器名称或ip,端口可以填变量值吗?

  7. BZOJ 3238 【AHOI2013】 差异

    题目链接:差异 写题时发现这道题当初已经用后缀数组写过了……但是既然学了后缀自动机那就再写一遍吧…… 观察一下题目所给的式子:\[\sum_{1\leqslant i < j \leqslant ...

  8. django urlencode

    from django.utils.http import urlquote a = urlquote('分享') print(a)

  9. 使用 Redis

    Redis(https://redis.io/),既不像 SQLite 以表的形式存储数据,也不像 MongoDB 允许以嵌套结构存储和查询,它是一种内存数据库结构,即将数据缓存在内存中.它将键—值( ...

  10. git branch 误删 分支 找回

    今天在写东西的时候,把自己的dev分支给删掉了.并且手贱的是吧远程分支也删掉了. 但是好在我强大的git!!!,把他找回来是分分钟的事情. 一.查看一下提交记录 git log -g 会有一个列表.如 ...