FORTRAN 与 .NET 互操作性介绍





5.00/5 (24投票s)
FORTRAN 和 C# .NET 代码混合语言编程奇妙世界的入门
引言
Fortran(以前称为 FORTRAN)是大多数程序员可能永远不会使用的语言之一。Fortran 几乎肯定比你年龄大,最初是用纸穿孔卡片编写的,这些卡片用于控制比你的数字手表处理能力还低的猛犸象大小的计算机。虽然 Fortran 是 IBM 在 50 年代中期设计的,但它并没有像猛犸象一样灭绝。Fortran 实际上仍然广泛用于数值计算,并且有大量现有的代码是用 Fortran 编写的。虽然 Fortran 肯定不会为你的花哨 MVC Web 框架和 NoSQL 数据库提供动力,但当你从事工程或科学领域的某个组织时,你很可能会遇到它。想想航空航天、机械工程、国防、空间和天文学。
那么 Fortran 除了可能在大型组织的地下室隐藏着大量现有代码之外,还有什么呢?为什么我应该关心呢?好吧,最新的 Fortran 版本实际上可以追溯到 2008 年,新版本计划于 2016 年发布。Fortran 擅长解决数值问题,并且被许多人认为是相对“安全”的语言,因为它在编译时有严格的检查。这使得它在许多情况下能够免疫缓冲区溢出等问题。
然而,Fortran 不是用于构建图形用户界面、生成 HTML 代码或消费和发布 Web 服务的语言。那么,如何在利用 Fortran 优点的同时,仍然能够为现代编程的所有其他方面使用正确的工具呢?解决方案在于不同语言之间的互操作性,也称为混合语言编程。
本系列旨在介绍混合语言编程的一些概念,并为有兴趣的人提供示例。
- FORTRAN 与 .NET 互操作性简介
- 交换复杂数据
- ISO C 绑定模块
- 回调函数和字符串
- 混合模式程序集
与 C# .NET 的互操作性
那么如何在不同语言之间实现这种互操作性(简称 interop)呢?本系列将解释使 Fortran 和 C# 代码在 .NET 框架上互操作所涉及的内容。假设主程序在 .NET 框架上运行;C# 代码将调用 Fortran 代码。可能涉及回调,但原生 Fortran 代码本身永远不会调用任何 C# 代码。
Fortran 中的调用约定
为了使两种语言能够很好地协同工作,首要需要弄清楚的是调用约定。调用约定指定了参数和返回值如何在函数调用之间传递。
对于 Fortran,没有规定的调用约定,因此由编译器设计者选择一个。大多数现代 Fortran 编译器似乎都使用称为 stdcall
的调用约定。在 stdcall
调用约定中,被调用者(被调用的函数)负责清理堆栈。Stdcall
被广泛使用,但例如纯 C 代码使用不同的调用约定,称为 cdecl
。唯一真正确定的方法是检查编译器文档或明确强制执行调用约定。
我们稍后会看到,.NET 框架允许在调用本机代码时指定要使用的调用约定,这与在 C/C++ 函数上指定调用约定类似。
有关调用约定的更多信息可以在 https://en.wikipedia.org/wiki/X86_calling_conventions 找到。
值传递和数据类型
接下来要弄清楚的重要事情是参数如何在函数之间传递以及两种语言之间哪些数据类型对应。首先,我们将看看参数是如何传递的。
Fortran 默认假定所有参数都按引用传递。这意味着当一个参数传递给一个函数时,只有存储该值的内存位置的地址被传输到堆栈上。在 .NET 框架中,所谓的原始类型(包括整数、双精度浮点数、字符等类型)按值传递,而所有类实例都按引用传递。
.NET 原始类型传递方式与 Fortran 期望参数传递方式之间的差异很重要,必须牢记。忘记这些差异可能导致堆栈损坏并导致接收到未定义的值。
将 Fortran 链接为共享库
为了能够将 Fortran 代码链接为共享库,Fortran 代码需要链接为共享库。这在 Fortran 中不常见;Fortran 模块通常被编译为目标文件,最终静态链接形成一个包含所有代码的单个可执行文件。我认为不言而喻,当混合 .NET 和 Fortran 时,后一种方法可能有点挑战。
幸运的是,大多数 Fortran 编译器套件都相当容易地支持将 Fortran 代码链接到共享库。我遇到的一个关键问题是,有时必须传递额外的编译器命令,以确保编译后的代码不假定固定的内存地址。这很重要,因为 Fortran 代码将加载到其他进程的内存空间中,而确切的内存位置无法事先保证。
命名约定和名称修饰
当将库链接为共享库时,库中包含的不同函数会被“导出”。这基本上涉及到创建一个包含函数名和库中相应地址的表。为了防止名称“冲突”,即两个不同的函数在此导出表中最终具有相同的名称,会执行名称修饰。另一个原因是使函数名不区分大小写。在名称修饰期间,代码中声明的函数名会通过编译器以某种方式转换,以或多或少地确保不会发生名称冲突。然而,对于人类来说,这种名称修饰可能不那么明显;例如,看看 C++ 的名称修饰。
了解这种名称修饰至关重要,因为我们需要告诉 .NET 在导出表中查找哪个函数。有关各种编译器的名称修饰实践概述,请参见此处:https://en.wikipedia.org/wiki/Name_mangling#Name_mangling_in_Fortran
基本示例
现在我将介绍一个基本示例,其中 Fortran 代码片段从用 C# 编写的 .NET 应用程序调用。该示例将只向 Fortran 函数传递一个整数值,Fortran 函数将返回该值。它仅用作证明这种混合语言确实有效,并且它将为你提供一个开始实验的起点。在本系列的后面,我们将研究更复杂的示例。
Fortran 代码
对于这个例子,我们将定义一个简单的 Fortran 函数,它接受一个整数作为参数并返回它。该函数将被命名为 return_integer
。
module INTEROP
implicit none
! A simple function demonstrating returning a value.
function return_integer(input) result(output)
! Don't leave the calling convention to chance.
!GCC$ ATTRIBUTES CDECL :: return_integer
integer*4, intent(in) :: input
integer*4 :: output
output = input
end function
end module
需要注意的是,return_integer
函数是在 INTEROP
模块中定义的。您需要模块名称才能弄清楚名称修饰。
C# .NET 代码
为了能够调用 return_integer()
Fortran 函数,需要以下 C# 代码。我决定将外部函数定义放在一个名为 Interop
的单独类中。虽然您也可以将它们完美地放在任何其他位置,但我将在后面的示例中在此处放置更多的外部函数定义。这样,所有外部函数定义都 nicely 分组。
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace FortranInterop
{
public class Interop
{
public Interop()
{
}
/// <summary>
/// Returns the passed integer value.
/// </summary>
/// <remarks>
/// External function definition for the simple return_integer Fortran function.
/// Using CDECL calling convention and the gFortran mangled function name.
/// </remarks>
/// <param name="input">Integer value to return.</param>
/// <returns>Returns the input value.</returns>
[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl,
EntryPoint = "__interop_MOD_return_integer")]
public static extern int ReturnInteger(ref int input);
}
}
上述代码片段中需要注意的关键点是使用了 [DllImport]
属性和输入参数的 ref
指令。ref
指令是必要的,它告诉 .NET 框架输入整数将按引用传递,因为 Fortran 假定事情就是这样完成的。
[DllImport]
属性告诉 .NET 框架在哪里查找 Fortran 函数以及如何调用它。第一个参数“Interop.dll”告诉框架外部函数 ReturnInteger()
位于名为“Interop.dll”的库中。第二个参数告诉框架它应该使用 Cdecl
调用约定,这与我们告诉 Fortran 使用的约定一致。最后一个参数告诉框架在 Fortran 库中函数名是什么,这就是名称修饰的由来。入口点应该与我们示例中的修饰名“__interop_MOD_return_integer
”完全匹配。
Interop
类可以在您的代码中用于调用 ReturnInteger()
函数,该函数反过来调用 Fortran 中的 return_integer
函数。在示例代码中,这是在一个简单的控制台应用程序中完成的
using System;
namespace FortranInterop
{
class Program
{
static void Main(string[] args)
{
int value, result;
value = 5;
// Pass the input value as a reference.
// This is required to comply with the Fortran argument
// passing method.
result = Interop.ReturnInteger(ref value);
// Output the result and wait for a keystroke.
Console.WriteLine("Returned value: {0}", result);
Console.Read();
}
}
}
编译 Fortran
要编译 Fortran 代码,您需要一个 Fortran 编译器。我所知道的主要编译器是 Intel 编译器、HP 编译器和 GNU Fortran 编译器(简称 gFortran)。对于本文,我在 Windows 上使用了 gFortran 编译器,因为它免费,而且它允许我在 Windows 上使用 Visual Studio 来编写 .NET 代码。
这些代码示例应该可以使用任何其他 Fortran 编译器,只需进行少量修改即可。您最有可能遇到名称修饰和用于设置调用约定的编译器指令的问题。
获取 gFortran
gFortran 可以作为 MinGW 套件的一部分获得,该套件旨在为 Windows 提供一个最小的 GNU 编译器环境。我发现安装 MinGW 相当容易。在这里获取:http://www.mingw.org/。
编译
使用 gFortran 编译 Fortran 代码分两步完成。首先,我们使用 **-c -o** 开关将代码本身编译为目标文件,其次,我们使用 **-shared** 开关将编译后的代码链接为共享库。**-fPIC** 开关确保生成的代码不固定到某个内存地址,这是允许代码在运行时加载所必需的。然而,对于某些平台,此开关似乎不是必需的。
对我来说,命令如下。如果您认真开始开发某些东西,您可能希望将它们放入适当的 Makefile 中。
gfortran -O2 -g -fPIC -c -o D:\Documents\Work\Articles\Interop.o D:\Documents\Work\Articles\Interop.f90
gfortran -O2 -g -fPIC -shared D:\Documents\Work\Articles\Interop.o -o D:\Documents\Work\Articles\Interop.dll
非 Windows 环境
我第一次让 Fortran 和 .NET 互操作时,我被限制在类似 Linux 的平台上,这意味着我必须使用 Mono 而不是 Microsoft 的 .NET 框架。我可以确认所有上述内容都可以在 Linux 下的 Mono 上运行,只需进行极小的修改,而且这绝不是一个仅限 Windows 的聚会。
主要需要注意的是 Mono 定位程序集的方式。我不得不将 Fortran 库复制到 /lib/ 路径,并使用文件扩展名 .so 而不是 .dll。请注意,这意味着您还需要更新 [DllImport]
属性文件名。
运行示例
确保将依赖项复制到 .NET 程序可以找到它们的位置,然后在 Visual Studio 中运行。结果如下:
注意事项
正确的架构(x86/x64)
如果您正在使用 PC,很有可能您正在运行一个可以同时运行 32 位 x86 和 x64 64 位代码的平台。Windows 肯定可以。然而,重要的是要确保所有程序集(或库)都面向相同的架构。gFortran 似乎编译为 32 位 x86,因此请确保您的 .NET 应用程序面向相同的架构。
缺少依赖项
gFortran 编译的代码会注入一些您需要处理的依赖项。如果未能这样做,将导致抛出 DllNotFoundException
,这不会告诉您实际出了什么问题。确保至少以下库可用:
- libgcc_s_dw2-1.dll
- libgfortran-3.dll
- libquadmath-0.dll
最快的解决方案是将它们与您的库一起复制。在 Linux 系统上,这似乎不是必需的。
Zlib1.dll 缺失
当您在 Windows 上使用 MinGW 编译 Fortran 代码时,您可能会收到 Zlib1.dll 文件缺失的错误消息。显然,Zlib 库默认不随 MinGW
一起提供。您应该能够使用 MinGW
安装管理器 (guimain.exe) 安装它,或者自己下载并编译 Zlib 代码。
System.EntryPointNotFoundException
检查库是否确实导出了正在查找的函数名。在 Windows 上,可以使用 DumpBin
实用程序
Dumpbin.exe /EXPORTS yourlibrary.dll
正在查找的函数名称必须在 dumpbin
生成的导出列表中。
历史
- 2015年12月18日:初始版本