百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 软件资讯 > 正文

.NET AOT 的使用以及 .NET 与 Go 互相调用

dezehang 2024-11-22 13:04 1 浏览

目录

  • 背景

  • C# 部分

    • 环境要求

    • 创建一个控制台项目

    • 体验 AOT 编译

    • C# 调用库函数

    • 减少体积

    • C# 导出函数

    • C# 调用 C# 生成的 AOT

  • Golang 部分

    • 安装 GCC

    • Golang 导出函数

  • .NET C# 和 Golang 互调

    • C# 调用 Golang

    • Golang 调用 C#

    • 其他


背景

其实,规划这篇文章有一段时间了,但是比较懒,所以一直拖着没写。

本文主要介绍如何在 .NET 和 Go 语言中如何生成系统(Windows)动态链接库,又如何从代码中引用这些库中的函数。本文中主要采用.NET7作为示例。

(为什么没用.NET8,因为AOT是.NET7的特性,而这篇文章是在.NET7出来时写的)

在 .NET 部分,介绍如何使用 AOT、减少二进制文件大小、使用最新的 [LibraryImport] 导入库函数;

在 Go 语言部分,介绍如何使用 GCC 编译 Go 代码、如何通过 syscall 导入库函数。

在文章中会演示 .NET 和 Go 相互调用各自生成的动态链接库,以及对比两者之间的差异。

本文文章内容以及源代码,可以 https://github.com/whuanle/csharp_aot_golang 中找到,如果本文可以给你带来帮助,可以到 Github 点个星星嘛。

C# 部分

环境要求

SDK:.NET 7 SDKDesktop development with C++ workload

IDE:Visual Studio 2022

Desktop development with C++ workload 是一个工具集,里面包含 C++ 开发工具,需要在 Visual Studio Installer 中安装,如下图红框中所示。

创建一个控制台项目

首先创建一个 .NET 7 控制台项目,名称为 CsharpAot

打开项目之后,基本代码如图所示:

我们使用下面的代码做测试:

public class Program
{
static void Main()
{
Console.WriteLine("C# Aot!");
Console.ReadKey();
}
}

体验 AOT 编译

这一步,可以参考官方网站的更多说明:

https://learn.microsoft.com/zh-cn/dotnet/core/deploying/native-aot/

为了能够让项目发布时使用 AOT 模式,需要在项目文件中加上 <PublishAot>true</PublishAot> 选项。

然后使用 Visual Studio 发布项目。

发布项目的配置文件设置,需要按照下图进行配置。

AOT 跟 生成单个文件 两个选项不能同时使用,因为 AOT 本身就是单个文件。

配置完成后,点击 发布,然后打开 Release 目录,会看到如图所示的文件。

.exe 是独立的可执行文件,不需要再依赖 .NET Runtime 环境,这个程序可以放到其他没有安装 .NET 环境的机器中运行。

然后删除以下三个文件:

 CsharpAot.exp
CsharpAot.lib
CsharpAot.pdb

光用 .exe 即可运行,其他是调试符号等文件,不是必需的。

剩下 CsharpAot.exe 文件后,启动这个程序:

C# 调用库函数

这一部分的代码示例,是从笔者的一个开源项目中抽取出来的,这个项目封装了一些获取系统资源的接口,以及快速接入 Prometheus 监控。

不过很久没有更新了,最近没啥动力更新,读者可以点击这里了解一下这个项目:

https://github.com/whuanle/CZGL.SystemInfo/tree/net6.0/src/CZGL.SystemInfo/Memory

因为后续代码需要,所以现在请开启 “允许不安全代码”。

本小节的示例是通过使用 kernel32.dll 去调用 Windows 的内核 API(Win32 API),调用 GlobalMemoryStatusEx 函数 检索有关系统当前使用物理内存和虚拟内存的信息

使用到的 Win32 函数可参考:https://learn.microsoft.com/zh-cn/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex

关于 .NET 调用动态链接库的方式,在 .NET 7 之前,通过这样调用:

 [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern Boolean GlobalMemoryStatusEx(ref MemoryStatusExE lpBuffer);

在 .NET 7 中,出现了新的操作方式 [LibraryImport]

文档是这样介绍的:

Indicates that a source generator should create a function for marshalling arguments instead of relying on the runtime to generate an equivalent marshalling function at run time.

指示源生成器应创建用于编组参数的函数,而不是依赖运行库在运行时生成等效的编组函数。

简单来说,就是我们要使用 AOT 写代码,然后代码中引用到别的动态链接库时,需要使用 [LibraryImport] 引入这些函数。

笔者没有在 AOT 下测试过 [DllImport],读者感兴趣可以试试。

新建两个结构体 MEMORYSTATUS.csMemoryStatusExE.cs

MEMORYSTATUS.cs

public struct MEMORYSTATUS
{
internal UInt32 dwLength;
internal UInt32 dwMemoryLoad;
internal UInt32 dwTotalPhys;
internal UInt32 dwAvailPhys;
internal UInt32 dwTotalPageFile;
internal UInt32 dwAvailPageFile;
internal UInt32 dwTotalVirtual;
internal UInt32 dwAvailVirtual;
}

MemoryStatusExE.cs

public struct MemoryStatusExE
{
/// <summary>
/// 结构的大小,以字节为单位,必须在调用 GlobalMemoryStatusEx 之前设置此成员,可以用 Init 方法提前处理
/// </summary>
/// <remarks>应当使用本对象提供的 Init ,而不是使用构造函数!</remarks>
internal UInt32 dwLength;

/// <summary>
/// 一个介于 0 和 100 之间的数字,用于指定正在使用的物理内存的大致百分比(0 表示没有内存使用,100 表示内存已满)。
/// </summary>
internal UInt32 dwMemoryLoad;

/// <summary>
/// 实际物理内存量,以字节为单位
/// </summary>
internal UInt64 ullTotalPhys;

/// <summary>
/// 当前可用的物理内存量,以字节为单位。这是可以立即重用而无需先将其内容写入磁盘的物理内存量。它是备用列表、空闲列表和零列表的大小之和
/// </summary>
internal UInt64 ullAvailPhys;

/// <summary>
/// 系统或当前进程的当前已提交内存限制,以字节为单位,以较小者为准。要获得系统范围的承诺内存限制,请调用GetPerformanceInfo
/// </summary>
internal UInt64 ullTotalPageFile;

/// <summary>
/// 当前进程可以提交的最大内存量,以字节为单位。该值等于或小于系统范围的可用提交值。要计算整个系统的可承诺值,调用GetPerformanceInfo核减价值CommitTotal从价值CommitLimit
/// </summary>

internal UInt64 ullAvailPageFile;

/// <summary>
/// 调用进程的虚拟地址空间的用户模式部分的大小,以字节为单位。该值取决于进程类型、处理器类型和操作系统的配置。例如,对于 x86 处理器上的大多数 32 位进程,此值约为 2 GB,对于在启用4 GB 调整的系统上运行的具有大地址感知能力的 32 位进程约为 3 GB 。
/// </summary>

internal UInt64 ullTotalVirtual;

/// <summary>
/// 当前在调用进程的虚拟地址空间的用户模式部分中未保留和未提交的内存量,以字节为单位
/// </summary>
internal UInt64 ullAvailVirtual;


/// <summary>
/// 预订的。该值始终为 0
/// </summary>
internal UInt64 ullAvailExtendedVirtual;

internal void Refresh()
{
dwLength = checked((UInt32)Marshal.SizeOf(typeof(MemoryStatusExE)));
}
}

定义引用库函数的入口:

public static partial class Native
{

/// <summary>
/// 检索有关系统当前使用物理和虚拟内存的信息
/// </summary>
/// <param name="lpBuffer"></param>
/// <returns></returns>
[LibraryImport("Kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial Boolean GlobalMemoryStatusEx(ref MemoryStatusExE lpBuffer);
}

然后调用 Kernel32.dll 中的函数:

public class Program
{
static void Main()
{
var result = GetValue();
Console.WriteLine($"当前实际可用内存量:{result.ullAvailPhys / 1000 / 1000}MB");
Console.ReadKey();
}

/// <exception cref="Win32Exception"></exception>
public static MemoryStatusExE GetValue()
{
var memoryStatusEx = new MemoryStatusExE();
// 重新初始化结构的大小
memoryStatusEx.Refresh();
// 刷新值
if (!Native.GlobalMemoryStatusEx(ref memoryStatusEx)) throw new Win32Exception("无法获得内存信息");
return memoryStatusEx;
}
}

使用 AOT 发布项目,执行 CsharpAot.exe 文件。

减少体积

在前面两个例子中可以看到 CsharpAot.exe 文件大约在 3MB 左右,但是这个文件还是太大了,那么我们如何进一步减少 AOT 文件的大小呢?

读者可以从这里了解如何裁剪程序:https://learn.microsoft.com/zh-cn/dotnet/core/deploying/trimming/trim-self-contained

需要注意的是,裁剪是没有那么简单的,里面配置繁多,有一些选项不能同时使用,每个选项又能带来什么样的效果,这些选项可能会让开发者用得很迷茫。

经过笔者的大量测试,笔者选用了以下一些配置,能够达到很好的裁剪效果,供读者测试。

首先,引入一个库:

<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>

接着,在项目文件中加入以下选项:

<!--AOT 相关-->
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<RunAOTCompilation>True</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<PublishReadyToRunEmitSymbols>false</PublishReadyToRunEmitSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeUTF7Encoding>true</EnableUnsafeUTF7Encoding>
<InvariantGlobalization>true</InvariantGlobalization>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>true</MetadataUpdaterSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<IlcDisableReflection >true</IlcDisableReflection>

最后,发布项目。

吃惊!生成的可执行文件只有 1MB 了,而且还可以正常执行。

笔者注:虽然现在看起来 AOT 的文件很小了,但是如果使用到 HttpClientSystem.Text.Json 等库,哪怕只用到了一两个函数,最终包含这些库以及这些库使用到的依赖,生成的 AOT 文件会大得惊人。

所以,如果项目中使用到其他 nuget 包的时候,别想着生成的 AOT 能小多少!

C# 导出函数

这一步可以从时总的博客中学习更多:https://www.cnblogs.com/InCerry/p/CSharp-Dll-Export.html

PS:时总真的太强了。

在 C 语言中,导出一个函数的格式可以这样:

// MyCFuncs.h
#ifdef __cplusplus
extern "C" { // only need to export C interface if
// used by C++ source code
#endif

__declspec( dllimport ) void MyCFunc();
__declspec( dllimport ) void AnotherCFunc();

#ifdef __cplusplus
}
#endif

当代码编译之后,我们就可以通过引用生成的库文件,调用 MyCFuncAnotherCFunc 两个方法。

如果不导出的话,别的程序是无法调用库文件里面的函数。

因为 .NET 7 的 AOT 做了很多改进,因此,.NET 程序也可以导出函数了。

新建一个项目,名字就叫 CsharpExport 吧,我们接下来就在这里项目中编写我们的动态链接库。

添加一个 CsharpExport.cs 文件,内容如下:

using System.Runtime.InteropServices;

namespace CsharpExport
{
public class Export
{
[UnmanagedCallersOnly(EntryPoint = "Add")]
public static int Add(int a, int b)
{
return a + b;
}
}
}

然后在 .csproj 文件中,加上 PublishAot 选项。

然后通过以下命令发布项目,生成链接库:

 dotnet publish -p:NativeLib=Shared -r win-x64 -c Release

看起来还是比较大,为了继续裁剪体积,我们可以在 CsharpExport.csproj 中加入以下配置,以便生成更小的可执行文件。

<!--AOT 相关-->
<PublishAot>true</PublishAot>
<TrimMode>full</TrimMode>
<RunAOTCompilation>True</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<PublishReadyToRunEmitSymbols>false</PublishReadyToRunEmitSymbols>
<DebuggerSupport>false</DebuggerSupport>
<EnableUnsafeUTF7Encoding>true</EnableUnsafeUTF7Encoding>
<InvariantGlobalization>true</InvariantGlobalization>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>true</MetadataUpdaterSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<IlcDisableReflection >true</IlcDisableReflection>

C# 调用 C# 生成的 AOT

在本小节中,将使用 CsharpAot 项目调用 CsharpExport 生成的动态链接库。

CsharpExport.dll 复制到 CsharpAot 项目中,并配置 始终复制

CsharpAotNative 中加上:

 [LibraryImport("CsharpExport.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.I4)]
internal static partial Int32 Add(Int32 a, Int32 b);

然后在代码中使用:

 static void Main()
{
var result = Native.Add(1, 2);
Console.WriteLine($"1 + 2 = {result}");
Console.ReadKey();
}

在 Visual Studio 里启动 Debug 调试:

可以看到,是正常运行的。

接着,将 CsharpAot 项目发布为 AOT 后,再次执行:

可以看到,.NET AOT 调用 .NET AOT 的代码是没有问题的。

Golang 部分

Go 生成 Windows 动态链接库,需要安装 GCC,通过 GCC 编译代码生成对应平台的文件。

安装 GCC

需要安装 GCC 10.3,如果 GCC 版本太新,会导致编译 Go 代码失败。

打开 tdm-gcc 官网,通过此工具安装 GCC,官网地址:

https://jmeubank.github.io/tdm-gcc/download/

下载后,根据提示安装。

然后添加环境变量:

D:\TDM-GCC-64\bin

运行 gcc -v,检查是否安装成功,以及版本是否正确。

Golang 导出函数

本节的知识点是 cgo,读者可以从这里了解更多:

https://www.programmerall.com/article/11511112290/

新建一个 Go 项目:

新建一个 main.go 文件,文件内容如下:

package main

import (
"fmt"
)
import "C"

//export Start
func Start(arg string) {
fmt.Println(arg)
}

// 没用处
func main() {
}

在 Golang 中,要导出此文件中的函数,需要加上 import "C",并且 import "C" 需要使用独立一行放置。

//export {函数名称} 表示要导出的函数,注意,//export 之间 没有空格。

main.go 编译为动态链接库:

go build -ldflags "-s -w" -o main.dll -buildmode=c-shared main.go

不得不说,Go 编译出的文件,确实比 .NET AOT 小一些。

前面,笔者演示了 .NET AOT 调用 .NET AOT ,那么, Go 调用 Go 是否可以呢?

答案是:不可以。

因为 Go 编译出来的 动态链接库本身带有 runtime,Go 调用 main.dll 时 ,会出现异常。

具有情况可以通过 Go 官方仓库的 Issue 了解:https://github.com/golang/go/issues/22192

这个时候,.NET 加 1 分。

虽然 Go 不能调用 Go 的,但是 Go 可以调用 .NET 的。在文章后面会介绍。

虽然说 Go 不能调用自己,这里还是继续补全代码,进一步演示一下。

Go 通过动态链接库调用函数的示例:

func main() {
maindll := syscall.NewLazyDLL("main.dll")
start := maindll.NewProc("Start")

var v string = "测试代码"
var ptr uintptr = uintptr(unsafe.Pointer(&v))
start.Call(ptr)
}

代码执行后会报错:

.NET C# 和 Golang 互调

C# 调用 Golang

main.dll 文件复制放到 CsharpAot 项目中,设置 始终复制

然后在 Native 中添加以下代码:

 [LibraryImport("main.dll", SetLastError = true)]
internal static partial void Start(IntPtr arg);

调用 main.dll 中的函数:

 static void Main()
{
string arg = "让 Go 跑起来";
// 将申请非托管内存string转换为指针
IntPtr concatPointer = Marshal.StringToHGlobalAnsi(arg);
Native.Start(concatPointer);
Console.ReadKey();
}

在 .NET 中 string 是引用类型,而在 Go 语言中 string 是值类型,这个代码执行后,会出现什么结果呢?

执行结果是输出一个长数字。

笔者不太了解 Golang 内部的原理,不确定这个数字是不是 .NET string 传递了指针地址,然后 Go 把指针地址当字符串打印出来了。

因为在 C、Go、.NET 等语言中,关于 char、string 的内部处理方式不一样,因此这里的传递方式导致了跟我们的预期结果不一样。

接着,我们将 main.go 文件的 Start 函数改成:

//export Start
func Start(a,b int) int{
return a+b
}

然后执行命令重新生成动态链接库:

go build -ldflags "-s -w" -o main.dll -buildmode=c-shared main.go

main.dll 文件 复制到 CsharpAot 项目中,将 Start 函数引用改成:

 [LibraryImport("main.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.I4)]
internal static partial Int32 Start(int a, int b);

执行代码调用 Start 函数:

 static void Main()
{
var result = Native.Start(1, 2);
Console.WriteLine($"1 + 2 = {result}");
Console.ReadKey();
}

Golang 调用 C#

CsharpExport.dll 文件复制放到 Go 项目中。

main 的代码改成:

func main() {
maindll := syscall.NewLazyDLL("CsharpExport.dll")
start := maindll.NewProc("Add")

var a uintptr = uintptr(1)
var b uintptr = uintptr(2)
result, _, _ := start.Call(a, b)

fmt.Println(result)
}

将参数改成 19,再次执行:

其他

在本文中,笔者演示了 .NET AOT,虽然简单的示例看起来是正常的,体积也足够小,但是如果加入了实际业务中需要的代码,最终生成的 AOT 文件也是很大的。

例如,项目中使用 HttpClient 这个库,会发现里面加入了大量的依赖文件,导致生成的 AOT 文件很大。

在 .NET 的库中,很多时候设计了大量的重载,同一个代码有好几个变种方式,以及函数的调用链太长,这样会让生成的 AOT 文件变得比较臃肿。

目前来说, ASP.NET Core 还不支持 AOT,这也是一个问题。

在 C# 部分,演示了如果使用 C# 调用系统接口,这里读者可以了解一下 pinvoke:http://pinvoke.net/

这个库封装好了系统接口,开发者不需要自己撸一遍,通过这个库可以很轻松地调用系统接口,例如笔者最近在写 MAUI 项目,通过 Win32 API 控制桌面窗口,里面就使用到 pinvoke 简化了大量代码。

本文是笔者熬夜写的,比较赶,限于水平,文中可能会有错误的地方,望大佬不吝指教。


相关推荐

WIN10系统如何安装UG10.0

随着科技的不断进步与更新,现在有很多公司己经安装上了WIN10的系统以及使用UG10.0了,但很多人反映WIN10系统安装UG10.0不好装,以下详细介绍一下1如果WIN10系统没有自带有JAVA需...

自学UG编程的心得分享

为什么有的人3个月学会基本的UG建模画图编程,有的断断续续3——5年才学会,还有的人干了7年的加工中心还不会电脑画图编程。这是什么原因?1.顾虑太多,什么都想得到,什么都想一起抓,总是上班加班没时间,...

UG/NX 绘制一个捞笊(zhào)模型,或者也可以叫它漏勺?

今天我们来看看这个模型,起因是群里有小伙伴说要做一个捞笊的模型,看见这名字直接给我整懵了,然后他发了张家里漏勺的图片才知道原来这玩意还有个这种名字。这东西相信每个小伙伴家里都有吧,它的建模方法也比较...

再也不用为学UG编程发愁了!380集最新UG资料免费送

上期发的UG教程很多粉丝都领到了,收获越来越多的好评!有你们一直陪伴真的很高兴,谢谢各位粉丝!为了给大家提供更优质的资源,这两个月都在整理你们最关心的UG资源,都是多位编程工厂老师傅的工厂实战精华,真...

优胜原创UG_3-4-5轴后处理下载

反复上机调试,安全稳定可靠,请放心使用2020.11.21,修复YSUG4-5轴后处理锁轴输出...

青华模具学院-UG10.0安装文件说明

青华模具学院分享:今天我们来跟大家一起学习NX10.0版本的安装方法,网上有很多这个版本的安装视频以及方法图文,但到最终安装软件时仍有很多新手对安装仍然感到头痛,基于这样的情况,我们特别就NX10.0...

UGnx10安装说明

温馨提示,安装前,请退出杀毒软件,关闭防火墙,因为这些软件可能阻断NX主程序和许可程序间的通信,导致安装后,软件无法启动。1、解压下载后的压缩包,右键,选择‘’解压到UGNX10_64位正式版(csl...

正版UG软件,正版UG代理,正版软件和盗版软件的区别

大家都知道,UG软件是制造业必不可少的一款三维软件,广泛应用于:CAE(有限元分析),CAD(产品设计/模具设计),CAM(计算机辅助制造编程),那么有人不禁要问了,正版软件和盗版软件在使用上有明显区...

非常全面的UG加工模块中英对照(图标注释)

大家好,我是粥粥老师,听说很多同学都在学习UG但是没有学习资料和安装包,今天粥粥老师就全部打包好免费发放给你们,那么怎么获取全套资料图档安装包呢领取途径①关注②评论、点赞、转发③私信“UG或者...

腾讯自研Git客户端 UGit|Git 图形界面客户端

支持平台:#Windows#macOS腾讯推出的一款Git图形界面客户端,简化了Git的使用流程,特别适合处理大型项目和文件。支持直接提交和推送操作,避免在大规模项目中由于远程频繁变更而导致...

经典收藏:UG重用库的一些不为人知小技巧

免费领取UG产品编程、UG多轴UG模具编程、安装包安装教程图档资料关注私信我“领取资料”,即可免费领取完整版,感谢支持,爱你们哟,么么主题:UG后处理+仿真+外挂UG重用库的正确使用方法:首先有...

UG编程常用指令G、M代码,快收藏好

今天给大家分享数控编程常用的指令代码,希望对正在学习路上的你带来一丝丝帮助。最好的方法就是转发到自己空间,方便以后学习。对了,如果你还需要其他UG教程学习资料,CNC加工中心的一些参数,以及UG画图,...

UG NX7.0中文版从入门到精通

Unigraphics(简称UG)是一套功能强大的CAD/CAE/CAM应用软件,UGNX7是其最新版本。《UGNX7从入门到精通(中文版)》以UGNX7为平台,从工程应用的角度出发,通过基...

经典UG建模基础练习图纸

UG是目前工作中比较优秀拥有大量用户的一款机械模具产品行业三维设计软件,cam加工丶软件支持全中文汉化;能够带给用户更为非凡的设计与加工新体验。很多朋友私信小编问有没有UG建模练习图纸,今天给大家分享...

UG NC软件基础操作,如何设置UG草图精度

默认情况下我们绘制草图一般只保留一位小数,即使你输入多位小数软件也会自动四舍五入,这个你做一些国标的图还好,国标以毫米为单位,一般保留小数点后一位就够了,但如果你做的图是英制单位,那么保留一位小数肯定...