大家好,我是百芯,可以叫我老百姓(老百芯)[笑哭]
之后我会在这里和大家分享 DFM可制造性分析、PCB设计、DFM工具,电路设计等相关的知识,请大家多多指教。
今天给大家分享的是PCB mark 点、mark 点的作用、mark 点识别原理、mark 点定位的一般原理与步骤、PCB mark 点制作、mark 点设置原则。
一、基准点(mark点)是什么意思?mark点也叫基准点,也叫光学定位点,是贴片机使用时的定位点。由于PCB在大批量生产中为装配过程中的所有步骤提供了共同的可测量点,因此装配中使用的每个设备都可以准确定位电路图案以实现精度,通过mark点程序员就可以在加载程序后自动设置机器。
mark 点
mark 点
二、mark点在PCB板上的作用当我们要打板的时候,我们就会将 Gerber 文件发给制造商。如果要需要将组件与PCB组装在一起,我们还需要提供物料清单(BOM文件)以及坐标文件(PNP文件)。这些文件会用自动贴片机来获取这些信息,然后需要在PCB上找到一个或者多个电路板的实际物理点。
如果我们在电路板上使用mark 点就可以让机器更好的放置组件,准确度更高,而且不依赖机器公差或者人工的误差。
mark 点
三、mark点识别原理PCB 上的mark 点是表面贴装技术(T) 和自动光学检测(AOI) 等自动化机械使用的参考标准。该标记由一个远离任何其他可见地标的单独铜垫组成,没有基准标记,机器要么放置组件不正确,要么完全拒绝运行。然而,通过读取放置在 PCB 上的各种基准标记位置,自动化设备可以确定放置或扫描组件的确切位置。
不过大多数机器在技术上不会读取放置在 PCB 上的内容,相反,它识别mark 点焊盘的反射。
四、不同类型的mark点1、单板mark点
全局mark 点作用是单板上定位所有电路特征的位置,用于区别电路图形和PCB基准,是基于三个网络系统的定位,其中参考点位于左下端 0.0,另外两个在在X和Y轴的正方向。
全局mark点
2、局部mark点
局部mark点主要用来定位引脚多、引脚间距小(引脚距中心不大于0.65mm)的各元器件,辅助定位。
局部mark点
3、工艺边mark点
作用在拼接板上,辅助定位所有电路功能,辅助定位。
工艺边mark点
五、mark点定位的一般原则和步骤1、mark点形状
选择基准标记的位置后,就可以决定它们的显示方式了。虽然一些制造设备被编程为可以识别各种形状,如菱形、正方形或沙漏形,但并不是所有的机器都可以处理。还是建议使用比较普遍的圆形mark点。
为什么通常都使用圆形mark点?
圆形物体更容易被机器定位。对于 HAL 完成,圆形基准上的凸形仍将是圆形,而在方形基准上,例如,它可能不再是正方形。机器更容易找到圆形的中心。圆形的表面积最小。均匀蚀刻圆形形状。可以使用多个基准点,而不是效率较低的奇形怪状基准点,后者在理论上可能包含旋转信息,但难以处理。这是一个与传统电路板可能具有的功能最不同的功能,传统电路板主要是矩形。圆形安装孔可以兼作便宜的基准。机器视觉需要准确地找到基准点,然后估计其确切的中心,圆形是最优的。
mark点形状
2、mark点 尺寸
基准标记可以有多种尺寸,主要取决于装配的机器。
3.2mm 阻焊层开口直径和 1.6mm 裸铜直径或 2mm 阻焊层开口直径和 1mm 裸铜直径的尺寸基本上可以适用于所有的机器。
同一印刷电路板上的基准标记尺寸不应超过 25 µm,建议间隙区域的最小尺寸为中心标记半径的两倍。
参考点周围应该有一个空白区域,该区域没有任何其他电路元件或标记。空白区域的最小尺寸应为参考点半径的两倍。
PCB 基准尺寸通常为 1 到 3 毫米,主要取决于制造商使用的组装机器。一些制造商建议在电路板的角处添加 3 个基准点,因为这会提供 2 个角度对齐测量值,并允许贴片机推断出正确的方向。一些制造商会说明具体尺寸,这也取决于制造商使用的装配设备。
一般来说,阻焊层开口的直径应该是基准裸铜直径的两倍,此外,同一块电路板(全局和局部)上的 PCB 基准尺寸应该一致,变化不应超过 ~25 微米。
如果要组装 2 层板,则顶层和底层基准点应位于彼此之上。顶层和底层 PCB 基准尺寸应相同,包括阻焊层开口。
两种常见的 PCB mark点尺寸和阻焊层开口建议
局部基准往往小至 1 毫米,阻焊层开口为 2 毫米,上图中显示的 D-3D 规则,是因为有些制造商比较喜欢这种较大的阻焊层开口。
局部 PCB 基准尺寸通常不超过 1 毫米,以便进行走线布线并为其他组件留出空间。对于 0201 电阻或芯片大小的 BGA 等小型元件,组装机将足够精确,因此不需要本地基准,并且机器将准确知道您元件需要放置的位置。
3、mark点 边缘距离
避免将基准点靠在 PCB 的边缘,贴装机械通常使用夹具在组装期间将 PCB 锁定到位。如果夹具覆盖了基准点,则问题很严重。可以将基准标记置于距边缘至少 3 毫米的中心位置(建议 5 毫米,可以消除这些风险)
mark点 边缘距离
4、mark点 组成
mark点 组成由 3 部分组成:
顶部或底部铜层上的实心铜环阻焊层中的圆圈是我们需要对准的目标侧面的选项文本标签mark点 组成
5、mark 点 位置布局
需要在PCBA的四个角或对角线上,形成多点面定位,定位准确,距离越远越好。
1)pcb mark点
mark点 的布局位置由贴片机的PCB传输方式决定。当使用导轨传送PCB时,Mark不能放置在靠近夹持面或定位孔的位置,具体尺寸因贴片机而异。一般要求如下图 所示。
区域标记无法定位
定位针过程中,mark点无法定位。对边过程中,mark点 不能定位在夹边到边4mm 范围内。PCB mark点 位置应沿对角线放置,并且它们之间的距离应尽可能大。•对于长度小于200mm 的 PCB,至少应放置2 个标记,如下图。对于长度超过200mm的PCB,需要如图b 在PCB上放置4个mark点,沿着PCB长边的中心线或靠近中心线放置1或2个mark点 。PCB mark点 标记应沿着每个小板的对角线放置,如下图 所示。
PCB mark点 位置布局
2)局部 mark点
局部mark点 位置应满足以下要求: 对于超过 100 个引脚的 QFP 元件,应沿对角线放置 2 个 mark点 ,如图 a 所示。对于引脚数超过 160 的 QFP 元件,应在四个角放置 4 个标记,如图 b 所示。
局部mark 点
mark点
6、mark点 切口间隙
mark点周围的适当间隙至关重要。在焊盘周围放置一个开放区域(无铜、阻焊层、丝网印刷等)。有了这个空间,相机就可以在没有视觉干扰的情况下拾取标记。
开放空间的直径应至少是焊盘尺寸的两倍。因此,对于 2mm 的焊盘,你需要在其周围至少留出 4mm 的间隙区域。间隙区域的形状不太重要;圆形和方形区域是两种流行的设计。
mark点 切口间隙
7、mark点 材料
mark点 焊盘需要用电路板其余部分使用的金属完成。(记住,焊盘是用来反射光的。)因此,不要用阻焊层、丝网印刷或任何其他材料覆盖焊盘。
8、mark点 数量
三个基准点的数量是消除模板相对于 PCB 意外错位的最佳数字。
1)1 个mark点
只有一个基准标记可用,扫描软件无法确定 PCB 的正确旋转。一台机器实际上无法运行只有一个基准标记的 PCB。
2)2个 mark点
有两个可用的基准标记,机器可以正常运行。然而,这里有两个风险在起作用。
双标记设置提供了很好但通常不是很好的位置跟踪。如果使用的是细间距组件,可能就不会那么准确相反的基准点可能会导致操作员错误。如果将 PCB 倒置插入,机器可能仍会看到基准点并继续其愉快的工作。这种失误最好的情况是浪费时间,最坏的情况是导致灾难性的组件堆积或永久性 PCB 和设备损坏。3)3 个mark点
三个是正确运行 PCB 的最佳基准标记数,包括第三个基准标记可以为三角测量增加一个额外的点,从而提高整体精度。它还消除了错误旋转的板通过相机的任何可能性。
4)4 个mark点
虽然看起来添加四个点只能进一步提高准确性,但很少有更多的东西可以通过这一点获得。这里的主要缺点是第四个基准标记会重新引入处理倒置面板的危险。走这条路线时要格外小心。
mark 点
8、mark 点铜饰面
mark 点焊盘需要是平稳的以反映均匀的图像,铜标记镀有你选择的任何金属饰面。电镀和浸渍等工艺在均匀性方面是可靠的,而热风焊料的变化往往更大一些。
mark 点铜饰面
如果饰面的厚度有任何变化,则无确反映。虽然并非无法克服,但它确实迫使生产操作员花费额外的时间来恢复标记。根据问题的严重程度,就需要编辑软件程序以进行补偿,或完全重新焊接基准点。简而言之,修复需要花费大量时间。
9、mark 点 对比度
当 mark点标记与印制板基板材料之间存在高对比度时,可实现最佳性能。对于所有标记点,内部背景必须相同。
六、mark点怎么制作?器件孔接口器件和连接器多为插件式元件。插件的通孔直径比管脚直径大8~20mil,焊接时渗锡性好。需要注意的是线路板出厂时的孔径存在误差。近似误差为±0.05mm。每0.05mm为一钻。直径超过3.20mm,每0.1mm为一钻。因此,在设计器件孔径时,应将单位换算为毫米,孔径应设计为0.05的整数倍。制造商根据用户提供的钻孔数据设定钻孔工具的尺寸。钻具尺寸通常比用户要求的成型孔大0.1-0.15mm。越少越好。
mark 点制作
以上就是关于 PCB mark 点的知识,希望大家多多支持。
图片来源于网络
如果有什么错误或者建议,欢迎在评论区留言。
建筑是《我的世界》的一大乐趣,以下是一些建筑技巧:
规划建筑:在开始建造前,先制定一个规划图,考虑建筑的尺寸、外观和功能。
使用多种材料:尝试不同类型的方块和装饰,以增加建筑的视觉吸引力。
创意模式:如果你想尝试复杂的建筑项目,可以切换到创意模式,获得无限资源。
利用红石:红石是一种强大的资源,可用于创建复杂的机械装置和电路。
探险是《我的世界》的核心体验之一,以下是一些建议:
准备好装备:在冒险前,确保你有足够的食物、武器、防具和工具。
导航:在地图上标记有趣的地点,以防止迷路。
避免夜晚冒险:夜晚是危险的时候,尽量在白天进行探险。
收集资源:在探险中采集资源,如矿石、宝藏和材料。
我可以说,10个人当中,有9个人没有用过我现在要讲的这个操作。
我在使用AUTOCAD进行绘图设计的时候,这个操作是频繁使用的。希望你一定看到最后,会让你受益的。
UNDO + M :标记
UNDO + B :后退
UNDO,相信应该有人用过这个命令。但是大部分人只知道它是一个返回的命令,对它的另一个更好的用处几乎没有尝试过,那就是做标记的功能。
【步骤1】打个比方说,我现在已经画好了下面这样的一个图形(为了大家理解方便,我故意将图形的颜色设定为了红色)。接下来你要继续绘制图形,如果你还没有构思好方案,自己想先试试画一下,看看效果如何。这个时候,你就键盘输入UNDO命令,然后再输入M键。
UNDO命令+M键,标记
【步骤2】接着你就可以大胆地去试着画图了,比方说你画了下面这样的图形(蓝色是我为了大家理解方便,故意设定了的颜色),
继续方案的设计
【步骤3】画到这个时候,你对现在画的方案感觉不太满意,想返回到步骤1的状态,你就不需要一直按返回键了,你只需要输入UNDO命令,再按一下B键,就可以一键返回到步骤1的状态。
UNDO+B键,后退
讲到这里,相信大家已经明白了UNDO+M这个命令的使用方法了吧。
当我们在绘图过程中,经常会有好几种方案想先试一试的情况,这个时候你就使用这个方法,将会大大提高自己的工作效率,而且也不容易出错。让它一键返回到我们做了标记的地方即可。
希望大家在自己的工作中一定要去尝试一下今天我讲的。我甚至将这个命令制作成了一个宏命令,将它捆绑到了我的鼠标上,以方便我能迅速地调用它。
#头条创作挑战赛#
本章介绍如何与本机(非托管)动态链接库 (DLL) 和组件对象模型 (COM) 组件集成。除非另有说明,否则本章中提到的类型存在于 System 或 System.Runtime.InteropServices 命名空间中。
调用本机 DLL是缩写,允许您访问非托管DLL(Unix上的)中的函数,结构和回调。
例如,考虑在 Windows DLL 中定义的 MessageBox 函数,如下所示:
int MessageBox (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
可以通过声明同名的静态方法、应用 extern 关键字并添加 DllImport 属性来直接调用此函数:
using System;using System.Runtime.InteropServices;MessageBox (IntPtr.Zero, "Please do not press this again.", "Attention", 0);[DllImport("user32.dll")]static extern int MessageBox (IntPtr hWnd, string text, string caption, int type);
System.Windows 和 System.Windows.Forms 命名空间中的 MessageBox 类本身调用类似的非托管方法。
下面是一个 Ubuntu Linux 的 DllImport 示例:
Console.WriteLine ($"User ID: {getuid()}");[DllImport("libc")]static extern uint getuid();
CLR 包括一个封送拆收器,该封送拆收器知道如何在 .NET 类型和非托管类型之间转换参数和返回值。在 Windows 示例中,int 参数直接转换为函数所需的四字节整数,字符串参数转换为以 null 结尾的 Unicode 字符数组(以 UTF-16 编码)。IntPtr 是一种结构,旨在封装非托管句柄;它在 32 位平台上为 32 位宽,在 64 位平台上为 64 位宽。类似的翻译发生在Unix上。(从 C# 9 开始,您还可以使用 nint 类型,该类型映射到 IntPtr 。
类型和参数封送处理封送处理常见类型在非托管端,可以有多种方法来表示给定的数据类型。例如,字符串可以包含单字节 ANSI 字符或 UTF-16 Unicode 字符,并且可以是长度前缀、以 null 结尾或固定长度。使用 MarshalAs 属性,可以向 CLR 封送拆收器指定使用的变体,以便它可以提供正确的转换。下面是一个示例:
[DllImport("...")]static extern int Foo ( [MarshalAs (UnmanagedType.LPStr)] string s );
非托管类型枚举包括封送拆收器理解的所有 Win32 和 COM 类型。在这种情况下,封送拆收器被告知转换为LPStr,这是一个以空结尾的单字节ANSI字符串。
在 .NET 端,您还可以选择要使用的数据类型。例如,非托管句柄可以映射到 IntPtr 、int、uint、long 或 ulong。
注意大多数非托管句柄封装地址或指针,因此必须映射到 IntPtr 才能与 32 位和 64 位操作系统兼容。一个典型的例子是HWND。
通常,使用 Win32 和 POSIX 函数时,您会遇到一个整数参数,该参数接受一组常量,这些常量在C++头文件(如 )中定义。无需将这些常量定义为简单的 C# 常量,而是可以在枚举中定义它们。使用枚举可以使代码更整洁,并提高静态类型的安全性。我们在中提供了一个示例。
注意安装 Visual Studio Microsoft时,请确保安装C++头文件,即使您在“C++”类别中未选择任何其他文件也是如此。这是定义所有本机 Win32 常量的位置。然后,您可以通过在 Visual Studio 程序目录中搜索 来找到所有头文件。
在Unix上,POSIX标准定义了常量的名称,但是符合POSIX的Unix系统的单个实现可能会为这些常量分配不同的数值。您必须为所选操作系统使用正确的数值。同样,POSIX 为互操作调用中使用的结构定义了标准。结构中字段的顺序不是标准的固定的,Unix 实现可能会添加其他字段。定义函数和类型的C++头文件通常安装在 include 或 中。
将字符串从非托管代码接收回 .NET 需要进行一些内存管理。如果使用 StringBuilder 而不是字符串声明外部方法,则封送拆收器会自动执行此工作,如下所示:
StringBuilder s = new StringBuilder (256);GetWindowsDirectory (s, 256);Console.WriteLine (s);[DllImport("kernel32.dll")]static extern int GetWindowsDirectory (StringBuilder sb, int maxChars);
在Unix上,它的工作方式类似。以下调用 getcwd 以返回当前:
var sb = new StringBuilder (256);Console.WriteLine (getcwd (sb, sb.Capacity));[DllImport("libc")]static extern string getcwd (StringBuilder buf, int size);
尽管 StringBuilder 使用起来很方便,但它的效率有些低下,因为 CLR 必须执行额外的内存分配和复制。在性能热点中,可以通过改用 char[] 来避免此开销:
[DllImport ("kernel32.dll", CharSet = CharSet.Unicode)]static extern int GetWindowsDirectory (char[] buffer, int maxChars);
请注意,必须在 DllImport 属性中指定字符集。您还必须在调用函数后将输出字符串修剪为长度。您可以实现此目的,同时使用阵列池最小化内存分配(参见中的),如下所示:
string GetWindowsDirectory(){ var array = ArrayPool<char>.Shared.Rent (256); try { int length = GetWindowsDirectory (array, 256); return new string (array, 0, length).ToString(); } finally { ArrayPool<char>.Shared.Return (array); }}
(当然,这个例子是人为的,因为您可以通过内置的 Environment.GetFolderPath 方法获取 Windows 目录。
注意如果您不确定如何调用特定的 Win32 或 Unix 方法,如果您搜索方法名称和 ,您通常会在互联网上找到一个示例。对于Windows,站点 是一个旨在记录所有Win32签名的wiki。
封送处理类和结构有时,您需要将结构传递给非托管方法。例如,Win32 API 中的 GetSystemTime 定义如下:
void GetSystemTime (LPSYSTEMTIME lpSystemTime);
LPSYSTEMTIME符合以下C结构:
typedef struct _SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds;} SYSTEMTIME, *PSYSTEMTIME;
要调用 GetSystemTime ,我们必须定义一个与 C 结构匹配的 .NET 类或结构:
using System;using System.Runtime.InteropServices;[StructLayout(LayoutKind.Sequential)]class SystemTime{ public ushort Year; public ushort Month; public ushort DayOfWeek; public ushort Day; public ushort Hour; public ushort Minute; public ushort Second; public ushort Milliseconds;}
属性指示封送拆收器如何将每个字段映射到其非托管对应项。LayoutKind.Sequential 意味着我们希望字段在边界上按顺序对齐(您很快就会看到这意味着什么),就像它们在 C 结构中一样。此处的字段名称无关紧要;字段的顺序很重要。
现在我们可以调用 GetSystemTime:
SystemTime t = new SystemTime();GetSystemTime (t);Console.WriteLine (t.Year);[DllImport("kernel32.dll")]static extern void GetSystemTime (SystemTime t);
同样,在Unix上:
Console.WriteLine (GetSystemTime());static DateTime GetSystemTime(){ DateTime startOfUnixTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); Timespec tp = new Timespec(); int success = clock_gettime (0, ref tp); if (success != 0) throw new Exception ("Error checking the time."); return startOfUnixTime.AddSeconds (tp_sec).ToLocalTime(); }[DllImport("libc")]static extern int clock_gettime (int clk_id, ref Timespec tp);[StructLayout(LayoutKind.Sequential)]struct Timespec{ public long tv_sec; /* seconds */ public long tv_nsec; /* nanoseconds */}
在 C 和 C# 中,对象中的字段位于距离该对象地址 个字节的位置。不同之处在于,在 C# 程序中,CLR 通过使用字段标记查找此偏移量来查找此偏移量;C 字段名称直接编译为偏移量。例如,在 C 中,wDay 只是一个标记,用于表示 SystemTime 实例地址加上 24 个字节的任何内容。
为了提高访问速度,每个字段都放置在字段大小的倍数的偏移处。但是,该乘数限制为最大 x 字节,其中 是。在当前实现中,默认包大小为 8 个字节,因此由一个字节后跟一个(8 字节)长的结构占用 16 个字节,并且该字节后面的 7 个字节被浪费。您可以通过 StructLayout 属性的 Pack 属性指定包装大小来减少或消除这种浪费:这将使字段与指定的倍数的偏移对齐。因此,当包大小为 1 时,刚刚描述的结构将仅占用 9 个字节。您可以指定 1、2、4、8 或 16 字节的包大小。
StructLayout 属性还允许您指定显式字段偏移量(请参阅)。
进出封送在前面的示例中,我们将 SystemTime 实现为一个类。我们可以选择一个结构 — 前提是 GetSystemTime 是使用 ref 或 out 参数声明的:
[DllImport("kernel32.dll")]static extern void GetSystemTime (out SystemTime t);
在大多数情况下,C# 的方向参数语义与外部方法的工作方式相同。按值传递参数被复制到中,C# ref 参数被复制入/复制出,C# 输出参数被复制出来。但是,具有特殊转换的类型有一些例外。例如,数组类和 StringBuilder 类在从函数中出来时需要复制,因此它们是输入/输出。有时,使用 In 和 Out 属性重写此行为很有用。例如,如果一个数组应该是只读的,则 in 修饰符指示只复制进入函数的数组,而不是从函数中出来的数组:
static extern void Foo ( [In] int[] array);调用约定
非托管方法通过堆栈和(可选)CPU 寄存器接收参数和返回值。因为有不止一种方法可以实现这一点,所以出现了许多不同的协议。这些协议称为。
CLR 目前支持三种调用约定:StdCall、Cdeccl 和 ThisCall。
默认情况下,CLR 使用调用约定(该平台的标准约定)。在Windows上,它是StdCall,在Linux x86上,它是Cdecl。
如果非托管方法不遵循此默认值,则可以显式声明其调用约定,如下所示:
[DllImport ("MyLib.dll", CallingConvention=CallingConvention.Cdecl)]static extern void SomeFunc (...)
有点误导性的命名CallingConvention.WinApi指的是平台默认值。
来自非托管代码的回调C# 还允许外部函数通过回调调用 C# 代码。有两种方法可以完成回调:
通过函数指针(来自 C# 9)通过代表为了说明这一点,我们将在 中调用以下 Windows 函数,该函数枚举所有顶级窗口句柄:
BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
WNDENUMPROC 是一个回调,它按顺序使用每个窗口的句柄触发(或直到回调返回 false)。以下是它的定义:
BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);使用函数指针的回调
从 C# 9 开始,当回调是静态方法时,最简单且性能最高的选项是使用。在 WNDENUMPROC 回调的情况下,我们可以使用以下函数指针:
delegate*<IntPtr, IntPtr, bool>
这表示一个接受两个 IntPtr 参数并返回布尔值的函数。然后,您可以使用 & 运算符为其提供静态方法:
using System;using System.Runtime.InteropServices;unsafe{ EnumWindows (&PrintWindow, IntPtr.Zero); [DllImport ("user32.dll")] static extern int EnumWindows ( delegate*<IntPtr, IntPtr, bool> hWnd, IntPtr lParam); static bool PrintWindow (IntPtr hWnd, IntPtr lParam) { Console.WriteLine (hWnd.ToInt64()); return true; }}
对于函数指针,回调必须是静态方法(或静态本地函数,如本例所示)。
仅限非托管呼叫者通过将非托管关键字应用于函数指针声明,并将 [UnmanagedCallersOnly] 属性应用于回调方法,可以提高性能:
using System;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;unsafe{ EnumWindows (&PrintWindow, IntPtr.Zero); [DllImport ("user32.dll")] static extern int EnumWindows ( delegate* unmanaged <IntPtr, IntPtr, byte> hWnd, IntPtr lParam); [UnmanagedCallersOnly] static byte PrintWindow (IntPtr hWnd, IntPtr lParam) { Console.WriteLine (hWnd.ToInt64()); return 1; }}
此属性标记 PrintWindow 方法,以便从非托管代码调用该方法,从而允许运行时采用快捷方式。请注意,我们还将方法的返回类型从布尔值更改为字节:这是因为应用 [UnmanagedCallersOnly] 的方法只能在签名中使用 值类型。可直接封送类型是不需要任何特殊封送逻辑的类型,因为它们在托管和非托管世界中的表示方式相同。其中包括基元整型、浮点型、双精度型和仅包含可拼接类型的结构。char 类型也是可 blitable,如果结构的一部分具有指定 CharSet.Unicode 的 StructLayout 属性:
[StructLayout (LayoutKind.Sequential, CharSet=CharSet.Unicode)]非默认调用约定
默认情况下,编译器假定非托管回调遵循平台默认调用约定。如果不是这样,您可以通过 [UnmanagedCallersOnly] 属性的 CallConvs 参数显式声明其调用约定:
[UnmanagedCallersOnly (CallConvs = new[] { typeof (CallConvStdcall) })]static byte PrintWindow (IntPtr hWnd, IntPtr lParam) ...
还必须通过在非托管关键字后插入特殊修饰符来更新函数指针类型:
delegate* unmanaged[Stdcall] <IntPtr, IntPtr, byte> hWnd, IntPtr lParam);注意
编译器允许您将任何标识符(如 XYZ)放在方括号内,只要存在调用的 .NET 类型(运行时可以理解该类型,并且与您在应用 [UnmanagedCallersOnly] 属性时指定的内容匹配)。这使Microsoft将来更容易添加新的调用约定。CallConvXYZ
在本例中,我们指定了 StdCall,这是 Windows 的平台默认值(Cdecl 是 Linux x86 的默认值)。以下是当前支持的所有选项:
名字 | 非托管修饰符 | 支撑类型 |
标准呼叫 | 非托管[标准呼叫] | CallConvStdcall |
中环 | 非托管[Cdecl] | CallConvCdecl |
此调用 | 非托管[此调用] | CallConvThiscall |
非托管回调也可以通过委托完成。此方法适用于所有版本的 C#,并允许引用实例的回调。
若要继续,请首先声明一个具有与回调匹配的签名的委托类型。然后,可以将委托实例传递给外部方法:
class CallbackFun{ delegate bool EnumWindowsCallback (IntPtr hWnd, IntPtr lParam); [DllImport("user32.dll")] static extern int EnumWindows (EnumWindowsCallback hWnd, IntPtr lParam); static bool PrintWindow (IntPtr hWnd, IntPtr lParam) { Console.WriteLine (hWnd.ToInt64()); return true; } static readonly EnumWindowsCallback printWindowFunc = PrintWindow; static void Main() => EnumWindows (printWindowFunc, IntPtr.Zero);}
具有讽刺意味的是,将委托用于非托管回调是不安全的,因为很容易陷入允许在委托实例超出范围后发生回调的陷阱(此时委托有资格进行垃圾回收)。这可能会导致最糟糕的运行时异常 - 没有有用的堆栈跟踪。对于静态方法回调,可以通过将委托实例分配给只读静态字段来避免这种情况(如本例所示)。对于实例方法回调,此模式无济于事,因此您必须仔细编码,以确保在任何潜在回调期间至少维护一个对委托实例的引用。即便如此,如果非托管端存在错误(即在您告诉它不要调用回调后),您可能仍然需要处理无法追踪的异常。解决方法是为每个非托管函数定义唯一的委托类型:这有助于诊断,因为委托类型在异常中报告。
您可以通过将 [UnmanagedFunctionPointer] 属性应用于委托,从平台默认值更改回调的调用约定:
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]delegate void MyCallback (int foo, short bar);模拟 C 联合
结构中的每个字段都有足够的空间来存储其数据。考虑一个包含一个 int 和一个字符的结构。int 可能从偏移量 0 开始,并保证至少四个字节。因此,字符将从至少 4 的偏移量开始。如果由于某种原因,char 从偏移量 2 开始,如果您为 char 分配了一个值,您将更改 int 的值。听起来像是混乱,不是吗?奇怪的是,C 语言支持一种称为的结构的变体,它正是这样做的。可以在 C# 中使用 LayoutKind.Explicit 和 FieldOffset 属性来模拟这种情况。
想出一个有用的情况可能具有挑战性。但是,假设您想在外部合成器上演奏音符。Windows Multimedia API 提供了一个通过 MIDI 协议执行此操作的功能:
[DllImport ("winmm.dll")]public static extern uint midiOutShortMsg (IntPtr handle, uint message);
第二个参数 消息 ,描述了要演奏的音符。问题在于构造这个 32 位无符号整数:它在内部划分为字节,代表 MIDI 通道、音符和打击速度。一种解决方案是通过按位<<、>> 和 |运算符将这些字节与 32 位“打包”消息相互转换。但是,更简单的是定义具有显式的结构:
[StructLayout (LayoutKind.Explicit)]public struct NoteMessage{ [FieldOffset(0)] public uint PackedMsg; // 4 bytes long [FieldOffset(0)] public byte Channel; // FieldOffset also at 0 [FieldOffset(1)] public byte Note; [FieldOffset(2)] public byte Velocity;}
通道、注释和速度字段故意与 32 位打包消息重叠。这允许您使用其中任何一个进行读取和写入。无需计算即可使其他字段保持同步:
NoteMessage n = new NoteMessage();Console.WriteLine (n.PackedMsg); // 0n.Channel = 10;n.Note = 100;n.Velocity = 50;Console.WriteLine (n.PackedMsg); // 3302410n.PackedMsg = 3328010;Console.WriteLine (n.Note); // 200共享内存
内存映射文件或是 Windows 中的一项功能,它允许同一台计算机上的多个进程共享数据。共享内存速度极快,与管道不同,它提供对共享数据的访问。我们在中看到了如何使用 MemoryMappedFile 类来访问内存映射文件;绕过这一点并直接调用 Win32 方法是演示 P/Invoke 的好方法。
Win32 CreateFileMapping 函数分配共享内存。您告诉它您需要多少字节以及用于标识共享的名称。然后,另一个应用程序可以通过调用具有相同名称的 OpenFileMapping 来订阅此内存。这两种方法都返回一个,您可以通过调用 MapViewOfFile 将其转换为指针。
下面是一个封装对共享内存的访问的类:
using System;using System.Runtime.InteropServices;using System.ComponentModel;public sealed class SharedMem : IDisposable{ // Here we're using enums because they're safer than constants enum FileProtection : uint // constants from winnt.h { ReadOnly = 2, ReadWrite = 4 } enum FileRights : uint // constants from WinBASE.h { Read = 4, Write = 2, ReadWrite = Read + Write } static readonly IntPtr NoFileHandle = new IntPtr (-1); [DllImport ("kernel32.dll", SetLastError = true)] static extern IntPtr CreateFileMapping (IntPtr hFile, int lpAttributes, FileProtection flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); [DllImport ("kernel32.dll", SetLastError=true)] static extern IntPtr OpenFileMapping (FileRights dwDesiredAccess, bool bInheritHandle, string lpName); [DllImport ("kernel32.dll", SetLastError = true)] static extern IntPtr MapViewOfFile (IntPtr hFileMappingObject, FileRights dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint dwNumberOfBytesToMap); [DllImport ("Kernel32.dll", SetLastError = true)] static extern bool UnmapViewOfFile (IntPtr map); [DllImport ("kernel32.dll", SetLastError = true)] static extern int CloseHandle (IntPtr hObject); IntPtr fileHandle, fileMap; public IntPtr Root => fileMap; public SharedMem (string name, bool existing, uint sizeInBytes) { if (existing) fileHandle = OpenFileMapping (FileRights.ReadWrite, false, name); else fileHandle = CreateFileMapping (NoFileHandle, 0, FileProtection.ReadWrite, 0, sizeInBytes, name); if (fileHandle == IntPtr.Zero) throw new Win32Exception(); // Obtain a read/write map for the entire file fileMap = MapViewOfFile (fileHandle, FileRights.ReadWrite, 0, 0, 0); if (fileMap == IntPtr.Zero) throw new Win32Exception(); } public void Dispose() { if (fileMap != IntPtr.Zero) UnmapViewOfFile (fileMap); if (fileHandle != IntPtr.Zero) CloseHandle (fileHandle); fileMap = fileHandle = IntPtr.Zero; }}
在此示例中,我们在使用 SetLastError 协议发出错误代码的 DllImport 方法上设置 SetLastError=true。这可确保在引发该异常时填充 Win32Exception 的错误详细信息。(它还允许您通过调用 Marshal.GetLastWin32Error 来显式查询错误。
为了演示这个类,我们需要运行两个应用程序。第一个创建共享内存,如下所示:
using (SharedMem sm = new SharedMem ("MyShare", false, 1000)){ IntPtr root = sm.Root; // I have shared memory! Console.ReadLine(); // Here's where we start a second app...}
第二个应用程序通过构造同名的 SharedMem 对象来订阅共享内存,现有参数为 true:
using (SharedMem sm = new SharedMem ("MyShare", true, 1000)){ IntPtr root = sm.Root; // I have the same shared memory! // ...}
最终结果是每个程序都有一个 IntPtr,一个指向同一非托管内存的指针。这两个应用程序现在需要以某种方式通过这个公共指针读取和写入内存。一种方法是编写一个封装所有共享数据的类,然后使用 UnmanagedMemoryStream 将数据序列化(和反序列化)到非托管内存。但是,如果有大量数据,这是低效的。想象一下,如果共享内存类有一兆字节的数据,并且只需要更新一个整数。更好的方法是将共享数据构造定义为结构,然后将其直接映射到共享内存中。我们将在下一节中讨论这个问题。
将结构映射到非托管内存可以直接将具有顺序或显式结构布局的结构映射到非托管内存。请考虑以下结构:
[StructLayout (LayoutKind.Sequential)]unsafe struct MySharedData{ public int Value; public char Letter; public fixed float Numbers [50];}
固定指令允许我们内联定义固定长度的值类型数组,这就是将我们带入不安全领域的原因。此结构中的空间以内联方式分配给 50 个浮点数。与标准 C# 数组不同,Numbers 不是对数组。如果我们运行以下内容
static unsafe void Main() => Console.WriteLine (sizeof (MySharedData));
结果为 208:50 个四字节浮点数,加上 Value 整数的四个字节,加上字母字符的两个字节。总数 206 四舍五入为 208,因为浮点数在四字节边界上对齐(四个字节是浮点数的大小)。
我们可以在不安全的上下文中演示 MySharedData,最简单的是使用堆栈分配的内存:
MySharedData d;MySharedData* data = &d; // Get the address of ddata->Value = 123;data->Letter = 'X';data->Numbers[10] = 1.45f;or:// Allocate the array on the stack:MySharedData* data = stackalloc MySharedData[1];data->Value = 123;data->Letter = 'X';data->Numbers[10] = 1.45f;
当然,我们并没有展示在托管环境中无法实现的任何内容。但是,假设我们要将 MySharedData 的实例存储在 CLR 垃圾回收器范围之外的非上。这就是指针变得非常有用的地方:
MySharedData* data = (MySharedData*) Marshal.AllocHGlobal (sizeof (MySharedData)).ToPointer();data->Value = 123;data->Letter = 'X';data->Numbers[10] = 1.45f;
Marshal.AllocHGlobal 在非托管堆上分配内存。以下是稍后释放相同内存的方法:
Marshal.FreeHGlobal (new IntPtr (data));
(忘记释放内存的结果是一个很好的老式内存泄漏。
注意从 .NET 6 开始,可以改为使用新的 NativeMemory 类来分配和释放非托管内存。NativeMemory 使用比 AllocHGlobal 更新(更好)的底层 API,还包括执行对齐分配的方法。
为了与它的名字保持一致,这里我们将MySharedData与我们在上一节中编写的SharedMem类结合使用。以下程序分配共享内存块,然后将 MySharedData 结构映射到该内存中:
static unsafe void Main(){ using (SharedMem sm = new SharedMem ("MyShare", false, (uint) sizeof (MySharedData))) { void* root = sm.Root.ToPointer(); MySharedData* data = (MySharedData*) root; data->Value = 123; data->Letter = 'X'; data->Numbers[10] = 1.45f; Console.WriteLine ("Written to shared memory"); Console.ReadLine(); Console.WriteLine ("Value is " + data->Value); Console.WriteLine ("Letter is " + data->Letter); Console.WriteLine ("11th Number is " + data->Numbers[10]); Console.ReadLine(); }}注意
您可以使用内置的 MemoryMappedFile 类而不是 SharedMem ,如下所示:
using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("MyShare", 1000))using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor()){ byte* pointer = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer); void* root = pointer; ...}
下面是附加到同一共享内存的第二个程序,读取第一个程序写入的值(它必须在第一个程序等待 ReadLine 语句时运行,因为共享内存对象在离开其 using 语句时被释放):
static unsafe void Main(){ using (SharedMem sm = new SharedMem ("MyShare", true, (uint) sizeof (MySharedData))) { void* root = sm.Root.ToPointer(); MySharedData* data = (MySharedData*) root; Console.WriteLine ("Value is " + data->Value); Console.WriteLine ("Letter is " + data->Letter); Console.WriteLine ("11th Number is " + data->Numbers[10]); // Our turn to update values in shared memory! data->Value++; data->Letter = '!'; data->Numbers[10] = 987.5f; Console.WriteLine ("Updated shared memory"); Console.ReadLine(); }}
每个程序的输出如下所示:
// First program:Written to shared memoryValue is 124Letter is !11th Number is 987.5// Second program:Value is 123Letter is X11th Number is 1.45Updated shared memory
不要被指针吓倒:C++程序员在整个应用程序中使用它们,并且能够让一切正常工作。至少大多数时候是这样!相比之下,这种用法相当简单。
碰巧的是,我们的例子不安全 - 确切地说 - 出于另一个原因。我们没有考虑两个程序同时访问同一内存时出现的线程安全(或者更准确地说,进程安全)问题。若要在生产应用程序中使用它,我们需要将 volatile 关键字添加到 MySharedData 结构中的“值”和“字母”字段中,以防止实时 (JIT) 编译器(或 CPU 寄存器中的硬件)缓存字段。此外,随着我们与字段的交互变得不平凡,我们很可能需要通过跨进程互斥来保护它们的访问,就像我们使用 lock 语句来保护对多线程程序中字段的访问一样。我们在中详细讨论了线程安全性。
固定和固定 {...}将结构直接映射到内存的一个限制是结构只能包含非托管类型。例如,如果需要共享字符串数据,则必须改用固定字符数组。这意味着手动转换到字符串类型或从字符串类型转换。具体操作方法如下:
[StructLayout (LayoutKind.Sequential)]unsafe struct MySharedData{ ... // Allocate space for 200 chars (i.e., 400 bytes). const int MessageSize = 200; fixed char message [MessageSize]; // One would most likely put this code into a helper class: public string Message { get { fixed (char* cp = message) return new string (cp); } set { fixed (char* cp = message) { int i = 0; for (; i < value.Length && i < MessageSize - 1; i++) cp [i] = value [i]; // Add the null terminator cp [i] = '\0'; } } }}注意
没有对固定数组的引用;相反,你会得到一个指针。当您索引到固定数组时,您实际上是在执行指针算术!
第一次使用 fixed 关键字时,我们为结构中的 200 个字符分配内联空间。同一关键字(有些令人困惑)在以后在属性定义中使用时具有不同的含义。它指示 CLR 固定对象,以便如果它决定在块内执行垃圾回收,它不会在内存堆上移动基础结构(因为它的内容是通过直接内存指针迭代的)。看看我们的程序,你可能想知道MySharedData是如何在内存中移动的,因为它不是驻留在堆上,而是驻留在非托管的世界中,垃圾收集器没有管辖权。但是,编译器不知道这一点,并且托管上下文中使用MySharedData,因此它坚持添加固定关键字以使不安全的代码在托管上下文中安全。编译器确实有一点 - 以下是将MySharedData放在堆上所需的全部内容:
object obj = new MySharedData();
这将生成一个带盒的 MySharedData - 在堆上,并且有资格在垃圾回收期间进行传输。
此示例说明如何在映射到非托管内存的结构中表示字符串。对于更复杂的类型,还可以选择使用现有的序列化代码。一个限制条件是序列化数据的长度不得超过其在结构中的空间分配;否则,结果是与后续字段的意外联合。
COM 互操作性.NET 运行时为 COM 提供特殊支持,使 COM 对象能够从 .NET 使用,反之亦然。COM 仅在 Windows 上可用。
COM 的目的COM 是组件对象模型的首字母缩写,组件对象模型是与库接口的二进制标准,由 Microsoft 于 1993 年发布。发明 COM 的动机是使组件能够以独立于语言和版本容错的方式相互通信。在 COM 之前,Windows 中的方法是发布使用 C 编程语言声明结构和函数的 DLL。这种方法不仅是特定于语言的,而且也很脆弱。这种库中类型的规范与其实现密不可分:即使使用新字段更新结构也意味着破坏其规范。
COM 的美妙之处在于通过称为 的构造将类型的规范与其基础实现分开。COM 还允许在有状态上调用方法,而不是局限于简单的过程调用。
注意在某种程度上,.NET 编程模型是 COM 编程原则的演变:.NET 平台还促进了跨语言开发,并允许二进制组件在不破坏依赖于它们的应用程序的情况下发展。
COM 类型系统的基础知识COM 类型系统围绕接口旋转。COM 接口很像 .NET 接口,但它更普遍,因为 COM 类型通过接口公开其功能。例如,在 .NET 世界中,我们可以简单地声明一个类型,如下所示:
public class Foo{ public string Test() => "Hello, world";}
这种类型的消费者可以直接使用 Foo。如果我们后来更改了 Test() 的,调用程序集将不需要重新编译。在这方面,.NET 将接口与实现分开,不需要接口。我们甚至可以在不破坏调用者的情况下添加重载:
public string Test (string s) => $"Hello, world {s}";
在COM世界中,Foo通过接口公开其功能以实现相同的解耦。因此,在Foo的类型库中,将存在这样的接口:
public interface IFoo { string Test(); }
(我们通过显示 C# 接口(而不是 COM 接口)来说明这一点。然而,原理是相同的 - 尽管管道不同。
然后,呼叫者将与IFoo而不是Foo进行交互。
在添加测试的重载版本时,COM的生活比.NET更复杂。首先,我们将避免修改 IFoo 接口,因为这会破坏与以前版本的二进制兼容性(COM 的原则之一是接口一旦发布,就是的)。其次,COM 不允许方法重载。解决方案是让Foo实现:
public interface IFoo2 { string Test (string s); }
(同样,为了熟悉起见,我们已将其音译为 .NET 界面。
支持多个接口对于使 COM 库至关重要。
IUnknown和IDispatch所有 COM 接口都使用全局唯一标识符 (GUID) 进行标识。
COM 中的根接口是 IUnknown — 所有 COM 对象都必须实现它。此接口有三种方法:
地址参考释放查询接口AddRef 和 Release 用于生存期管理,因为 COM 使用引用计数而不是自动垃圾回收(COM 旨在处理非托管代码,其中自动垃圾回收不可行)。方法返回支持该接口的对象引用(如果可以)。
要启用动态编程(例如,脚本和自动化),COM 对象还可以实现 IDispatch 。这使得动态语言(如 VBScript)能够以后期绑定的方式调用 COM 对象,就像 C# 中的动态语言(尽管仅用于简单调用)。
从 C 调用 COM 组件#CLR 对 COM 的内置支持意味着您不直接使用 和 IDispatch。相反,您使用 CLR 对象,运行时通过运行时可调用包装器 (RCW) 封送对 COM 世界的调用。运行时还通过调用 AddRef 和 Release(当 .NET 对象完成时)来处理生存期管理,并处理两个世界之间的基元类型转换。例如,类型转换可确保每一端都能看到熟悉形式的整数和字符串类型。
此外,还需要有某种方法以静态类型方式访问 RCW。这是 工作。COM 互操作类型是自动生成的代理类型,用于为每个 COM 成员公开一个 .NET 成员。类型库导入程序工具 () 基于您选择的 COM 库从命令行生成 COM 互操作类型,并将其编译为 。
注意如果 COM 组件实现多个接口,则 工具将生成一个类型,其中包含来自所有接口的成员的联合。
可以在 Visual Studio 中创建 COM 互操作程序集,方法是转到“添加引用”对话框,然后从“COM”选项卡中选择一个库。例如,如果您安装了Microsoft Excel,则添加对Microsoft Excel 对象库的引用允许您与 Excel 的 COM 类进行互操作。下面是用于创建和显示工作簿,然后在该工作簿中填充单元格的 C# 代码:
using System;using Excel = Microsoft.Office.Interop.Excel;var excel = new Excel.Application();excel.Visible = true;Excel.Workbook workBook = excel.Workbooks.Add();((Excel.Range)excel.Cells[1, 1]).Font.FontStyle = "Bold";((Excel.Range)excel.Cells[1, 1]).Value2 = "Hello World";workBook.SaveAs (@"d:\temp.xlsx");注意
当前需要在应用程序中嵌入互操作类型(否则,运行时不会在运行时找到它们)。单击 Visual Studio 的解决方案资源管理器中的 COM 引用,并在“属性”窗口中将“嵌入互操作类型”属性设置为 true,或者打开 文件并添加以下行(粗体):
<ItemGroup> <COMReference Include="Microsoft.Office.Excel.dll"> ... <EmbedInteropTypes>true</EmbedInteropTypes> </COMReference></ItemGroup>
Excel.Application 类是一种 COM 互操作类型,其运行时类型为 RCW。当我们访问工作簿和单元格属性时,我们会返回更多的互操作类型。
可选参数和命名参数由于 COM API 不支持函数重载,因此具有大量参数的函数是很常见的,其中许多参数是可选的。例如,下面介绍了如何调用 Excel 工作簿的 Save 方法:
var missing = System.Reflection.Missing.Value;workBook.SaveAs (@"d:\temp.xlsx", missing, missing, missing, missing, missing, Excel.XlSaveAsAccessMode.xlNoChange, missing, missing, missing, missing, missing);
好消息是 C# 对可选参数的支持是 COM 感知的,因此我们可以这样做:
workBook.SaveAs (@"d:\temp.xlsx");
(正如我们在第中所述,可选参数由编译器“扩展”为完整的详细形式。
命名参数允许您指定其他参数,无论其位置如何:
workBook.SaveAs (@"c:\test.xlsx", Password:"foo");隐式引用参数
某些 COM API(尤其是 Word Microsoft)公开将参数声明为按引用传递的函数,无论函数是否修改参数值。这是因为不复制参数值可以感知到性能增益(性能增益可以忽略不计)。
从历史上看,从 C# 调用此类方法很笨拙,因为必须为每个参数指定 ref 关键字,这会阻止使用可选参数。例如,要打开Word文档,我们过去必须这样做:
object filename = "foo.doc";object notUsed1 = Missing.Value;object notUsed2 = Missing.Value;object notUsed3 = Missing.Value;...Open (ref filename, ref notUsed1, ref notUsed2, ref notUsed3, ...);
借助隐式 ref 参数,您可以在 COM 函数调用中省略 ref 修饰符,从而允许使用可选参数:
word.Open ("foo.doc");
需要注意的是,如果您调用的 COM 方法确实改变了参数值,则不会收到编译时或运行时错误。
索引省略 ref 修饰符的功能还有另一个好处:它使具有 ref 参数的 COM 索引器可通过普通 C# 索引器语法访问。否则将禁止这样做,因为 C# 索引器不支持 ref / out 参数。
还可以调用接受参数的 COM 属性。在下面的示例中,Foo 是一个接受整数参数的属性:
myComObject.Foo [123] = "Hello";
仍然禁止自己在 C# 中编写此类属性:类型只能在其自身(“默认”索引器)上公开索引器。因此,如果要用 C# 编写使上述语句合法的代码,Foo 需要返回另一个公开(默认)索引器的类型。
动态绑定动态绑定可以通过两种方式在调用 COM 组件时提供帮助。
第一种方法是允许在没有 COM 互操作类型的情况下访问 COM 组件。为此,请使用 COM 组件名称调用 Type.GetTypeFromProgID 以获取 COM 实例,然后从此使用动态绑定调用成员。当然,没有智能感知,编译时检查是不可能的:
Type excelAppType = Type.GetTypeFromProgID ("Excel.Application", true);dynamic excel = Activator.CreateInstance (excelAppType);excel.Visible = true;dynamic wb = excel.Workbooks.Add();excel.Cells [1, 1].Value2 = "foo";
(同样的事情也可以实现,但更笨拙,用反射而不是动态绑定。
注意此主题的变体是调用支持 IDispatch 的 COM 组件。然而,这样的组件非常罕见。
动态绑定在处理 COM 变体类型时也很有用(在较小程度上)。由于设计不佳而不是必要性的原因,COM API 函数通常充斥着这种类型,大致相当于 .NET 中的对象。如果在项目中启用“嵌入互操作类型”(稍后会详细介绍),运行时会将变量映射到动态,而不是将变量映射到对象,从而避免了强制转换的需要。例如,你可以合法地做
excel.Cells [1, 1].Font.FontStyle = "Bold";
而不是:
var range = (Excel.Range) excel.Cells [1, 1];range.Font.FontStyle = "Bold";
以这种方式工作的缺点是会丢失自动完成功能,因此您必须知道名为 Font 的属性恰好存在。因此,将结果分配给其已知的互操作类型通常更容易:
Excel.Range range = excel.Cells [1, 1];range.Font.FontStyle = "Bold";
如您所见,这比老式方法仅节省了五个字符!
变量到动态的映射是默认设置,并且是在引用上启用嵌入互操作类型的功能。
嵌入互操作类型我们之前说过,C# 通常通过通过调用 工具(直接或通过 Visual Studio)生成的互操作类型来调用 COM 组件。
过去,唯一的选择是引用互操作程序集,就像任何其他程序集一样。这可能会很麻烦,因为使用复杂的 COM 组件时,互操作程序集可能会变得非常大。例如,Microsoft Word 的小型加载项需要一个比自身大几个数量级的互操作程序集。
您可以选择嵌入所使用的部分,而不是引用互操作程序集。编译器分析程序集以精确地计算出应用程序所需的类型和成员,并直接在应用程序中嵌入(仅)这些类型和成员的定义。这避免了膨胀以及需要发送其他文件。
若要启用此功能,请在 Visual Studio 的解决方案资源管理器中选择 COM 引用,然后在“属性”窗口中将“嵌入互操作类型”设置为 true,或者按照前面所述编辑 文件(请参阅)。
类型等效性CLR 支持链接互操作类型的。这意味着,如果两个程序集分别链接到一个互操作类型,则如果这些类型包装相同的 COM 类型,则它们将被视为等效。即使它们链接到的互操作程序集是独立生成的,也是如此。
注意类型等效依赖于 System.Runtime.InteropServices 命名空间中的 TypeIdentifierAttribute 属性。编译器会在链接到互操作程序集时自动应用此属性。如果 COM 类型具有相同的 GUID,则认为它们是等效的。
向 COM 公开 C# 对象还可以使用 C# 编写可在 COM 世界中使用的类。CLR 通过称为 (CCW) 的代理使这成为可能。CCW 封送在两个世界之间进行类型(与 RCW 一样),并根据 COM 协议的要求实现 IUnknown(以及可选的 IDispatch)。CCW 通过引用计数(而不是通过 CLR 的垃圾回收器)从 COM 端进行生存期控制。
可以将任何公共类公开给 COM(作为“进程内”服务器)。为此,请首先创建一个接口,为其分配唯一的 GUID(在 Visual Studio 中,可以使用工具>),声明它对 COM 可见,然后设置接口类型:
namespace MyCom{ [ComVisible(true)] [Guid ("226E5561-C68E-4B2B-BD28-25103ABCA3B1")] // Change this GUID [InterfaceType (ComInterfaceType.InterfaceIsIUnknown)] public interface IServer { int Fibonacci(); }}
接下来,提供接口的实现,为该实现分配唯一的 GUID:
namespace MyCom{ [ComVisible(true)] [Guid ("09E01FCD-9970-4DB3-B537-0EC555967DD9")] // Change this GUID public class Server { public ulong Fibonacci (ulong whichTerm) { if (whichTerm < 1) throw new ArgumentException ("..."); ulong a = 0; ulong b = 1; for (ulong i = 0; i < whichTerm; i++) { ulong tmp = a; a = b; b = tmp + b; } return a; } }}
编辑您的 . 文件,添加以下行(粗体):
<PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <EnableComHosting>true</EnableComHosting></PropertyGroup>
现在,当您生成项目时,会生成一个附加文件 ,该文件可以注册为 COM 互操作。(请记住,根据您的项目配置,文件将始终为 32 位或 64 位:在这种情况下没有“任何 CPU”这样的东西。从的命令提示符下,切换到保存 DLL 的目录并运行 。
然后,可以从大多数支持 COM 的语言使用 COM 组件。例如,可以在文本编辑器中创建此 Visual Basic 脚本,并通过在 Windows 资源管理器中双击该文件来运行它,或者像启动程序一样从命令提示符启动它:
REM Save file as ComClient.vbsDim objSet obj = CreateObject("MyCom.Server")result = obj.Fibonacci(12)Wscript.Echo result
请注意,.NET Framework 不能加载到与 .NET 5+ 或 .NET Core 相同的进程中。因此,.NET 5+ COM 服务器不能加载到 .NET Framework COM 客户端进程中,反之亦然。
启用无注册表 COM传统上,COM 将类型信息添加到注册表。无注册表 COM 使用清单文件而不是注册表来控制对象激活。若要启用此功能,请将以下行(粗体)添加到 文件:
<PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <EnableComHosting>true</EnableComHosting> <EnableRegFreeCom>true</EnableRegFreeCom></PropertyGroup>
然后,您的构建将生成。
注意.NET 5+ 不支持生成 COM 类型库 (*.tlb)。您可以手动编写 IDL(接口定义语言)文件或接口中本机声明的C++标头。