都是用 DllImport?有没有考虑过自己写一个 extern 方法?
你做 .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 方法?的更多相关文章
- 写一个 sum方法,在使用下面任一语法调用时,都可以正常工作
		console.log(sum(2,3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5 (至少)有两种方法可以做到: 方法1: functio ... 
- Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。
		#29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ... 
- mysql 的 help 命令:每个命令,都有相应的反斜杠(\)加一个字母或字符的简写
		mysql> help For information about MySQL products and services, visit: http://www.mysql.com/ For d ... 
- 获取器操作都是针对数据而不是数据集的,要通过append()方法添加数据表不存在的字段
		获取器操作都是针对数据而不是数据集的,要通过append()方法添加数据表不存在的字段 public function getMembership(){ //加入会员s_id = 1 $busines ... 
- Java基础面试操作题: 线程问题,写一个死锁(原理:只有互相都等待对方放弃资源才会产生死锁)
		package com.swift; public class DeadLock implements Runnable { private boolean flag; DeadLock(boolea ... 
- 编写自定义PE结构的程序(如何手写一个PE,高级编译器都是编译好的PE头部,例如MASM,TASM等,NASM,FASM是低级编译器.可以自定义结构)
		正在学PE结构...感谢个位大哥的文章和资料...这里先说声谢谢 一般高级编译器都是编译好的PE头部,例如MASM,TASM等一直都说NASM,FASM是低级编译器.可以自定义结构但是苦于无人发布相关 ... 
- Java项目中每一个类都可以有一个main方法
		Java项目中每一个类都可以有一个main方法,但只有一个main方法会被执行,其他main方法可以对类进行单元测试. public class StaticTest { public static ... 
- 都别说工资低了,我们来一起写简单的dom选择器吧!
		前言 我师父(http://www.cnblogs.com/aaronjs/)说应当阅读框架(jquery),所以老夫就准备开始看了 然后公司的师兄原来写了个dom选择器,感觉不错啊!!!原来自己从来 ... 
- 编写一段程序,从标准输入读取string对象的序列直到连续出现两个相同的单词或者所有单词都读完为止。使用while循环一次读取一个单词,当一个单词连续出现两次是使用break语句终止循环。输出连续重复出现的单词,或者输出一个消息说明没有人任何单词是重复出现的。
		// test14.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #include< ... 
随机推荐
- 2018 China Collegiate Programming Contest Final (CCPC-Final 2018)
			Problem A. Mischievous Problem Setter 签到. #include <bits/stdc++.h> using namespace std; #defin ... 
- 2016-2017 CT S03E07: Codeforces Trainings Season 3 Episode 7
			B. Pen Pineapple Apple Pen Solved. 题意:将一个序列合并成一个数. 思路:分类讨论一下, 水. #include<bits/stdc++.h> using ... 
- 【android】如何让WebView对Video标签的支持更强力
			先说结论:各个产商对HTML5特性支持的程度不一样,用默认的WebChromeClient不能普遍适用. 因此咱基于GITHUB上一个VideoEnabledWebView库做了自己的封装,在魅族.华 ... 
- dubbo熔断,限流,服务降级
			1 写在前面 1.1 名词解释 consumer表示服务调用方 provider标示服务提供方,dubbo里面一般就这么讲. 下面的A调用B服务,一般是泛指调用B服务里面的一个接口. 1.2 拓扑图 ... 
- 20155201 实验五《Java面向对象程序设计》实验报告
			20155201 实验五<Java面向对象程序设计>实验报告 一.实验内容 1. 数据结构应用 2. 结对编程:利用IDEA完成网络编程任务,1人负责客户端,1人负责服务器 3. 密码结对 ... 
- HBase相关概念
			1.Row Key 基本原则是:(1).由于读取数据只能依靠RowKey,所以应把经常使用到的字段作为行键{如手机号+时间戳拼接的字符串} (2).RowKey长度越短越好,最好不要超过16个字节.从 ... 
- 使用awk分割字符串并且获取分割后的最后一个字符串
			示例:从字符串"you-me-he"中获取he echo "you-me-he" |awk -F '[-]' '{print $NF}' 
- Vim 的光标移动定位
			一.光标移动以单个字符为单位: 在命令模式中 h向左 l 向右 j 向上 k 向下 二.光标移动以word 为单位: w 将光标向前移动一个word; b 将光标向后移动一个word: 以上2个命令光 ... 
- 谈谈java中对象的深拷贝与浅拷贝
			知识点:java中关于Object.clone方法,对象的深拷贝与浅拷贝 引言: 在一些场景中,我们需要获取到一个对象的拷贝,这时候就可以用java中的Object.clone方法进行对象的复制,得到 ... 
- MySQL返回影响行数的测试示例
			found_rows() : select row_count() : update delete insert 注:需要配合相应的操作一起使用,否则返回的值只是1和-1(都是不正确的值) 示例: d ... 
