65.9K
CodeProject 正在变化。 阅读更多。
Home

Fortran 到 C# 的桥梁

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (16投票s)

2017年2月21日

CPOL

10分钟阅读

viewsIcon

26054

downloadIcon

414

本文讨论了从 Fortran 桥接到 .NET 语言 C#。

引言

本文讨论了从用 Fortran 编写的程序桥接到用 .NET 语言 C# 编写的 DLL。许多文章都致力于从 C 语言桥接到 Fortran。本文的目的是提供一个完整的从 Fortran 桥接到 C# 的示例。

简单来说,遗留系统的一部分由一个调用核心 DLL 的客户端程序组成;两者都用 Fortran 编写。调用程序由一个组织维护,核心 DLL 由另一个组织维护。然后,核心 DLL 被迁移到 .NET 语言 C#。任务是构建一个 Fortran 到 C# 的桥接,以

  1. 满足遗留接口并
  2. 支持核心 DLL 在 C# 中的迁移实现

设置

使用 Visual Studio 2015 和 Intel® Parallel Studio XE 2016 Update 3 Composer Edition for Fortran Windows* Integration for Microsoft Visual Studio 2015 包,创建了以下项目:

  1. Client – 一个 Visual Fortran 控制台应用程序,代表客户端程序
  2. Core – 一个 Visual Fortran 动态链接库,满足对客户端程序的遗留接口
  3. CppWrapper – 一个 Visual C++/CLR 类库,从原生 C++ 桥接到托管 C#
  4. 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 互操作。换句话说,ClientCppWrapper 库之间无法直接互操作。因此,有必要引入 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

请注意,Clientcore 方法中的 p7 参数已被 Core 库的 interface 定义中的两个参数替换:

  1. nos_p7 - 一个输入整数,包含 p7 数组的大小
  2. 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

请注意,输入整数 p1p4nos_p7 使用 VALUE 属性传递。这些参数与 CppWrapper 函数的相应形式参数类型可互操作(C_INT <-> int)。输出整数 p5p6 在没有 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# 的桥接,需要执行以下任务:

  1. 定义一个类型为 CppWrapper::DoWork 的变量 work
  2. 调用 workpublic 方法将原生参数转换为其托管对应项。
  3. 调用 CSharp Core 库方法,使用托管调用参数。
  4. 将托管输出参数转换回其非托管对应项。

诸如 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 对应项,需要执行以下两个步骤:

  1. 创建托管数组对象
    	// p7(out) - set pointer array
    	Int32 _nos_p7 = nos_p7;
    	array<String^>^ _p7 = gcnew array<String^>(_nos_p7);
  2. 用指针地址初始化它
    	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);

调用后,p3p7 等输出参数已被设置。有必要将这些托管变量复制回其非托管对应项。这借助 .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 类方法:StringToHGlobalAnsiFreeHGlobal [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));
	}

字符串数组 _p7CSharpCore 库中设置。上述代码将从 StringToHGlobalAnsi 返回的非托管内存写入指针 ptrs_p7 指向的缓冲区。这意味着输出参数 p7 的原始成员现在包含字符串 _p7

CSharpCore 库是在 MS Visual Studio 中创建的 C# 库。但是,由于讨论此库不会必然增强 Fortran 到 C# 桥接或 Fortran C 互操作性的说明性,因此将省略。

结论

本文的目的是演示如何从一种最受欢迎的遗留编程语言 Fortran 桥接到一种最受欢迎且功能强大的现代编程语言 C#。Fortran 到 C# 的桥接很重要,因为它允许在 .NET Framework 语言中实现遗留域逻辑。在此类环境中,软件开发的最佳实践、设计原则和现代工具更加易于实现。

参考文献

  1. https://gcc.gnu.org/wiki/GFortranStandards
  2. http://stackoverflow.com/questions/9686532/arrays-of-strings-in-fortran-c-bridges-using-iso-c-binding
  3. https://en.wikipedia.org/wiki/Row-_and_column-major_order
  4. Al Kelley and Ira Pohl, A Book on C. The Benjamin/Cummings Publishing Co., 1990.
  5. Nishant Sivakumar, C++/ CLI in Action. Manning Publications Co., 2007.
  6. https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal(v=vs.110).aspx
© . All rights reserved.