Fortran 到 C# 的桥梁






4.94/5 (16投票s)
本文讨论了从 Fortran 桥接到 .NET 语言 C#。
引言
本文讨论了从用 Fortran 编写的程序桥接到用 .NET 语言 C# 编写的 DLL。许多文章都致力于从 C 语言桥接到 Fortran。本文的目的是提供一个完整的从 Fortran 桥接到 C# 的示例。
简单来说,遗留系统的一部分由一个调用核心 DLL 的客户端程序组成;两者都用 Fortran 编写。调用程序由一个组织维护,核心 DLL 由另一个组织维护。然后,核心 DLL 被迁移到 .NET 语言 C#。任务是构建一个 Fortran 到 C# 的桥接,以
- 满足遗留接口并
- 支持核心 DLL 在 C# 中的迁移实现
设置
使用 Visual Studio 2015 和 Intel® Parallel Studio XE 2016 Update 3 Composer Edition for Fortran Windows* Integration for Microsoft Visual Studio 2015 包,创建了以下项目:
Client
– 一个 Visual Fortran 控制台应用程序,代表客户端程序Core
– 一个 Visual Fortran 动态链接库,满足对客户端程序的遗留接口CppWrapper
– 一个 Visual C++/CLR 类库,从原生 C++ 桥接到托管 C#CSharpCore
– 一个 Visual C# 类库,代表迁移的核心功能
这些项目中包含的代码仅为说明性质:它们侧重于简洁性和可读性,并且只包含展示 C 互操作性概念所需的编程构造。
客户端
Client
程序由另一个组织维护。出于演示目的,创建了一个简短的程序,其唯一目的是链接到 Core
库并调用其 core
方法。在 Client
程序的开头,导入了 Core
库。
!! import the CORE DLL
!DEC$ ATTRIBUTES DLLIMPORT :: CORE_METHOD
Client
调用 Core
库的 core
方法。方法名称及其参数由两个利益相关组织签署的接口文档指定。
!! call method from CORE DLL
CALL CORE_METHOD(p1, p2, p3, p4, p5, p6, p7, p8)
core
方法有 8 个调用参数:4 个输入参数和 4 个输出参数。输入参数使 Client
组织能够控制计算的各个方面和输出。输出参数将结果从 Core
组织传回 Client
组织。core
方法的参数在 Client
的变量部分定义。
!! variable declarations
INTEGER :: p1
REAL (KIND=precision), dimension(xdim_p2,ydim_p2) :: p2
REAL (KIND=precision), dimension(num_p3) :: p3
INTEGER :: p4
INTEGER :: p5
INTEGER :: p6
character(LEN=len_p7), dimension(num_p7), TARGET :: p7
INTEGER, dimension(num_p8) :: p8
特别是,core
方法的参数必须满足 interface
文档中陈述的要求。
名称 | 类型 | 维度 | 输入/输出 |
---|---|---|---|
p1 | int | - | input |
p2 | 二维双精度数组 | xdim_p2, ydim_p2 | input |
p3 | 一维双精度数组 | num_p3 | 输出 |
p4 | int | - | input |
p5 | int | - | 输出 |
p6 | int | - | 输出 |
p7 | 一维字符数组 | num_p7 | 输出 |
p8 | 一维整数数组 | num_p8 | input |
在附带的代码示例中,参数定义为:xdim_p2
= 4,ydim_p2
= 5,num_p3
= 8,num_p7
= 10,num_p8
= 5,precision
= 8,len_p7
= 80。
核心
Core
库由第二个组织维护。Core
库的目的是实现核心域逻辑。如上所述,最初用 Fortran 编写的 Core
库已迁移到 C# 库。
由于 Fortran 和 C++ 都是原生编程语言,因此应该可以直接从 Fortran 客户端调用 C++ 库的方法。关于引言中的图,这意味着可以直接从 Client
调用 CppWrapper
库的方法。
检查上一节中 Client
调用中参数的定义,我们看到参数 p7
是一个一维数组,其元素是长度为 80
的字符类型。根据 Fortran 标准的《C 语言互操作性》章节 [1],“如果类型是字符,则当且仅当其值等于 1 时,长度类型参数才可互操作”[2]。由于长度类型 len_p7
是 80 而不是 1,因此 p7
不能直接与 C 互操作。换句话说,Client
和 CppWrapper
库之间无法直接互操作。因此,有必要引入 Core
库。Core
库是一个 Fortran DLL,并在图中显示。
在深入探讨 p7
的细节之前,导出 core
方法供调用的 Client
访问的语法是:
!! export the CORE DLL
!DEC$ ATTRIBUTES DLLEXPORT :: CORE_METHOD
为了启用 C 互操作性,必须导入 ISO_C_BINDING
模块,该模块通过暴露原生 C 类型来支持 Fortran 与 C 的互操作性。
!! import package
USE ISO_C_BINDING, ONLY: C_INT, C_FLOAT, C_DOUBLE, C_CHAR, C_LOC, C_PTR, C_NULL_CHAR
ONLY 语法限制了可以从模块访问的实体。它还向读者确切地展示了程序中使用的实体。
Core
库的主要目的是为 CppWrapper
库定义一个 Fortran C 互操作接口。该接口访问 BIND
属性的 NAME
标签中显示的 CppWrapper
函数。
INTERFACE
SUBROUTINE CORE_FORTRAN_WRAPPER(p1, p2, p3, p4, p5, p6, nos_p7, ptrs_p7, p8) _
BIND(C,NAME='CORE_C_WRAPPER')
USE ISO_C_BINDING
implicit none
include "defs.fi"
INTEGER (C_INT), VALUE, intent(in) :: p1
REAL (KIND=8), dimension(xdim_p2,ydim_p2), intent(in) :: p2
REAL (KIND=8), dimension(num_p3), intent(out) :: p3
INTEGER (C_INT), VALUE, intent(in) :: p4
INTEGER (C_INT), intent(out) :: p5
INTEGER (C_INT), intent(out) :: p6
INTEGER (C_INT), VALUE, intent(in) :: nos_p7
TYPE (C_PTR), dimension(num_p7), intent(out) :: ptrs_p7
INTEGER (C_INT), dimension(num_p8), intent(in) :: p8
END SUBROUTINE CORE_FORTRAN_WRAPPER
END INTERFACE
请注意,Client
的 core
方法中的 p7
参数已被 Core
库的 interface
定义中的两个参数替换:
nos_p7
- 一个输入整数,包含p7
数组的大小ptrs_p7
- 一个指针数组,大小与p7
数组相同
也就是说,为了在 Fortran 和 C 之间传递 p7
(本质上是字符串数组),它被替换为一个指针数组,每个元素指向字符串第一个元素的地址。此方法在 [2] 中提出。通过重新定义此参数,接口中显示的 core
方法现在是 C 互操作的。
在调用此方法之前,我们只需设置指针数组 ptrs_p7
的值。
!! to pass an array of strings: assign to array of pointers and pass the pointer array
do i=1,num_p7
ptrs_p7(i) = C_LOC(p7(i))
end do
请注意,输入整数 p1
、p4
和 nos_p7
使用 VALUE
属性传递。这些参数与 CppWrapper
函数的相应形式参数类型可互操作(C_INT <-> int
)。输出整数 p5
和 p6
在没有 VALUE
属性的情况下传递。这些参数与 CppWrapper
函数的形式参数 *指针* 类型(引用类型)可互操作(C_INT <-> int*
)。CppWrapper
库在下一节中介绍。有关详细信息,请参阅 [1]。
最后,接口方法调用是:
!! call to FORTRAN wrapper
CALL CORE_FORTRAN_WRAPPER(p1, p2, p3, p4, p5, p6, nos_p7, ptrs_p7, p8)
CppWrapper
在 MS Visual Studio 中,已创建了一个具有公共语言运行时支持的 C++ 动态链接库,例如 C++/CLI DLL。在 CppWrapper
方法的实现中,原生参数被转换为 CLI 参数。完成此操作后,就可以调用 C# 核心方法。
CppWrapper.h
Fortran 原生库(上一节的 Core
库)的目标方法定义在文件 CppWrapper.h 中。如前所述,目标方法的名称在接口子程序中定义。
namespace CppWrapper {
extern "C" void API CORE_C_WRAPPER(
int p1,
double p2[][ParameterSize::xdim_p2],
double* p3,
int p4,
int* p5,
int* p6,
int nos_p7,
char** ptrs_p7,
int* p8);
}
参数 p2
是一个二维双精度数组。Fortran 和 C 中的数组声明是相反的:Fortran 数组按列主序分组,而 C 数组按行主序分组 [3]。为了说明这一点,请考虑二维数组 AF。
22 | 6 | 40 |
33 | 7 | 50 |
在 Fortran 中,AF 声明为 AF(2,3)
,AF 的元素按列主序存储。因此,内存分配是 {地址, 值} = {(1,22), (2,33), (3,6), (4,7), (5,40), (6,50)}。
现在考虑二维数组 AC。
22 | 33 |
6 | 7 |
40 | 50 |
在 C++ 中,AC 声明为 AC[3,2]
,并且由于 AC 的元素按行主序存储,内存分配是 {地址, 值} = {(0,22), (1,33), (2,6), (3,7), (4,40), (5,50)}。除了地址索引外,这与 AF(2,3)
的内存分配相同,例如 AF(2,3)
等同于 AC[3,2]
。因此,在将二维数组从 Fortran 传递到 C 时,C 互操作性要求反转数组维度参数。
其次,根据 C 语法,当二维数组是函数定义中的形式参数时,必须明确给出列大小 [4]。将这两个事实结合起来意味着对于接口中的 p2
数组,列值维度是 xdim_p2
。
ptrs_p7
数组是一个输出参数数组,包含指向 char
的指针,没有 VALUE
属性。因此,如上一节所述,它与形式参数 *指针* 类型(即 char**
)可互操作。出于同样的原因,p8
数组与 int*
可互操作。
CppWrapper.cpp
实现文件包含 CppWrapper
库函数和一个 CLI 类 DoWork
。此类引用 C# 程序集,并包含一个 public
方法,该方法负责在 CppWrapper
库函数和 CSharp Core 程序集方法之间转换参数。
因此,要完成到 C# 的桥接,需要执行以下任务:
- 定义一个类型为
CppWrapper::DoWork
的变量work
。 - 调用
work
的public
方法将原生参数转换为其托管对应项。 - 调用 CSharp
Core
库方法,使用托管调用参数。 - 将托管输出参数转换回其非托管对应项。
诸如 p1
之类的输入整数参数的处理方式非常直接。
// p1(in) Int32 _p1 = (Int32)p1;
对于多维数组 p2
,会创建一个托管数组来存储其元素。
// p2(in) - store in managed array
array<double, 2>^ _p2 = gcnew array<double, 2>(ParameterSize::xdim_p2, ParameterSize::ydim_p2);
模板语法的第一个参数定义了数组类型,第二个参数定义了其维度。gcnew
操作符在 CLI 堆上创建一个托管对象,并返回该对象的句柄。句柄(用 ^
表示)是对 CLI 托管堆上对象的引用。有关详细信息,请参阅 [5]。
使用以下代码填充二维数组 _p2
。
for (int j = 0; j < ParameterSize::ydim_p2; j++)
for (int i = 0; i < ParameterSize::xdim_p2; i++)
_p2[i, j] = *(p2[0, 0] + i + j*ParameterSize::xdim_p2);
如前所述,Fortran 中的多维数组以列主序的连续内存块存储。由于这意味着行索引变化最快,因此需要逐行填充数组。
对于输出双精度数组 p3
,必须创建一个托管数组以传递给 CLI 类方法。
// p3(out)
array<double>^ _p3 = gcnew array<double>(ParameterSize::num_p3);
为了传递诸如 p5
这样的输出整数指针参数,它被转换为托管的 IntPtr
。
// p5(out)
IntPtr ptr_p5 = (IntPtr)p5;
接下来,考虑 ptrs_p7
数组。为了将此数组转换为其 C++/CLI 对应项,需要执行以下两个步骤:
- 创建托管数组对象
// p7(out) - set pointer array Int32 _nos_p7 = nos_p7; array<String^>^ _p7 = gcnew array<String^>(_nos_p7);
- 用指针地址初始化它
for (int i = 0; i < _nos_p7; i++) { char* _chars = ptrs_p7[i]; String^ theString = gcnew String(_chars); _p7[i] = theString; }
p8
数组的处理方式类似。
现在所有参数都已重构为托管变量,就可以调用 CLI 库方法了。
// call
work.CORE_CLI_WRAPPER(_p1, _p2, _p3, _p4, ptr_p5, ptr_p6, _p7, _p8);
此方法的签名是
public:void CORE_CLI_WRAPPER(
Int32 p1,
array<double, 2>^% p2,
array<double>^% p3,
Int32 p4,
IntPtr ptr_p5,
IntPtr ptr_p6,
array<String^>^% p7,
array<Int32>^% p8)
这里,%
操作符是一个跟踪引用,其行为类似于 C++ 中的原生引用 δ。也就是说,就像在 C++ 中通过解引用 * 来获得 δ 一样;在 CLI 中通过解引用 ^
来获得 %
。同样,有关详细信息请参阅 [5]。在此方法的实现中,C# 方法是从 CSharpCore
库调用的。
// CSharpCore: static call
Methods::CoreMethod(p1, p2, p3, p4, ptr_p5, ptr_p6, p7, p8);
调用后,p3
或 p7
等输出参数已被设置。有必要将这些托管变量复制回其非托管对应项。这借助 .NET Framework 的 Marshal
类完成,该类位于 InteropServices
程序集中。
using namespace System::Runtime::InteropServices;
Marshal
类有一系列 copy
方法,具体取决于复制方向(托管到非托管或非托管到托管)和类型。以下 copy
命令从托管的 _p3
复制到非托管的 p3
,从位置 0
开始,长度为 _p3->Length
。
// p3(out) - copy from managed to unmanaged
Marshal::Copy(_p3, 0, IntPtr(p3), _p3->Length);
为了处理从托管到非托管的 string
复制(如输出参数 ptrs_p7
所需),还需要另外两个 Marshal
类方法:StringToHGlobalAnsi
和 FreeHGlobal
[6]。StringToHGlobalAnsi
方法将托管 string
复制到非托管内存,同时将其转换为 ANSI 格式。此外,此方法会分配 copy
所需的非托管内存。由于调用 StringToHGlobalAnsi
分配了非托管内存,因此有必要通过调用 FreeHGlobal
来释放它。
Int32 ns = 0;
for each (String^ str in _p7)
{
char* chars =
(char*)(Marshal::StringToHGlobalAnsi(str)).ToPointer();
sprintf_s(ptrs_p7[ns++], ParameterSize::len_p7, "%s", chars);
Marshal::FreeHGlobal(System::IntPtr((void*)chars));
}
字符串数组 _p7
在 CSharpCore
库中设置。上述代码将从 StringToHGlobalAnsi
返回的非托管内存写入指针 ptrs_p7
指向的缓冲区。这意味着输出参数 p7
的原始成员现在包含字符串 _p7
。
CSharpCore
库是在 MS Visual Studio 中创建的 C# 库。但是,由于讨论此库不会必然增强 Fortran 到 C# 桥接或 Fortran C 互操作性的说明性,因此将省略。
结论
本文的目的是演示如何从一种最受欢迎的遗留编程语言 Fortran 桥接到一种最受欢迎且功能强大的现代编程语言 C#。Fortran 到 C# 的桥接很重要,因为它允许在 .NET Framework 语言中实现遗留域逻辑。在此类环境中,软件开发的最佳实践、设计原则和现代工具更加易于实现。
参考文献
- https://gcc.gnu.org/wiki/GFortranStandards
- http://stackoverflow.com/questions/9686532/arrays-of-strings-in-fortran-c-bridges-using-iso-c-binding
- https://en.wikipedia.org/wiki/Row-_and_column-major_order
- Al Kelley and Ira Pohl, A Book on C. The Benjamin/Cummings Publishing Co., 1990.
- Nishant Sivakumar, C++/ CLI in Action. Manning Publications Co., 2007.
- https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal(v=vs.110).aspx