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

FORTRAN 与 .NET 的互操作性:复杂数据交换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (7投票s)

2016 年 5 月 13 日

MIT

12分钟阅读

viewsIcon

24220

downloadIcon

272

本系列文章《FORTRAN 与 .NET 的互操作性》的这部分将介绍如何在 C# 和 FORTRAN 之间交换复杂数据。

引言

在本系列的第一篇文章中,我们介绍了使 .NET 世界能够调用 Fortran 代码的基本要素。在那个示例中,我们只将一个简单的整数传递给 Fortran,然后将其返回给 .NET 代码,以展示基本概念的可行性。在实际应用中,您可能会发现自己需要向前和向后传递各种数据,甚至可能是结构化数据。本文将解释在 Fortran 和 .NET 世界之间传递数据的技术、技巧和注意事项。

  1. FORTRAN 与 .NET 互操作性简介
  2. 复杂数据交换
  3. ISO C 绑定模块
  4. 回调函数和字符串
  5. 混合模式程序集

那么,如何实现不同语言之间的这种互操作性(简称 interop)呢?本系列文章将解释在 .NET 框架上运行 Fortran 和 C# 代码进行互操作所涉及的内容。假定主程序运行在 .NET 框架上;C# 代码将调用 Fortran 代码。可能会涉及回调函数,但本地 Fortran 代码本身不会调用任何 C# 代码。

接收简单数据

在基础示例中,我们使用了一个返回值的 Fortran 函数来演示从 .NET 调用 Fortran 代码的过程。但在某些情况下,您可能希望从函数或子例程返回多个值,这与 C# 方法参数中的 out 声明类似。

在 Fortran 中,这种功能通过一种与从 Fortran 函数返回值类似的方式实现:您将参数放在参数列表中并声明其意图为 out。在 C# 端,您也需要将参数声明为 outref 也可以)。

以以下 Fortran 代码为例

! A subroutine demonstrating passing in and out a native Fortran integer type.
subroutine copy_integer(a, b)
    
    integer, intent(in) :: a
    integer, intent(out) :: b

    b = a

end subroutine

匹配的 C# 外部方法声明如下

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_copy_integer")]

public static extern void CopyInteger(ref int a, out int b);

处理数组

如果您在使用 Fortran 代码,那么很可能您会经常使用矩阵。正如我之前提到的,Fortran 在数值计算领域堪称王者,而在进行数值计算的地方,通常会涉及矩阵和向量。本节将向您介绍在 C# 和 Fortran 之间传递数组所需的技术。首先,我将讨论所有相关问题,并在本节末尾提供一些示例。

维度倒置

Fortran 与大多数其他编程语言的一个重要功能差异在于 Fortran 存储多维数组的方式。大多数语言按行存储多维数组,而 Fortran 按列存储。从数值角度来看,按列存储的方式更有意义,因为它就像存储一组向量,并且在较低级别上使矩阵运算稍微更直接。此外,这种差异通常对使用该语言的程序员来说并不太重要,除非(出乎意料地)您开始进行混合语言编程。

由于数组通常作为完整的内存块(或指向它们的指针,我们将在“封送”部分更深入地探讨)进行传输,因此数组元素在内存中的确切存储方式变得很重要。

固定长度数组

将固定长度数组传递给 Fortran 非常简单。只需在 Fortran 中将变量声明为已知长度的数组即可。在 C# 端,您可以简单地将数组传递给 Fortran 代码。您唯一需要注意的就是数组的长度是否正确。在 C# 外部方法声明中添加额外的封送指令可以防止意外传递错误大小的数组。比较以下两个外部方法声明

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_sum_array_fixed")]
public static extern double SumArrayFixed([MarshalAs(UnmanagedType.LPArray, SizeConst = 10)] 
double[] input);

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_sum_array_fixed")]
public static extern double SumArrayFixedSimple(double[] input);

第一个在输入参数上包含 MarshalAs 属性,其中 SizeConst 属性设置为 10。传递错误长度的数组将引发错误。

与这些方法声明匹配的 Fortran 代码如下

! Demonstrates passing a fixed length array to Fortran.
! Returns the sum of the values in the array.
function sum_array_fixed(data) result(sum)

    real*8, intent(in) :: data(10)
    real*8 :: sum
    integer :: i
    
    sum = 0
    do i = 1, 10
        sum = sum + data(i)
    end do
    
    sum = sum
    
end function

可变长度数组

将可变长度数组从 C# 传递到 Fortran 稍微复杂一些。通过将数组的长度作为参数传递给 Fortran 函数,我们可以使用该参数在 Fortran 端定义数组的长度。返回传递数组所有元素之和的示例 Fortran 代码如下

! Demonstrates passing a variable length array to Fortran.
! Returns the sum of the values in the array.
function sum_array(data, length) result(sum)

    integer*4, intent(in) :: length
    real*8, intent(in) :: data(length)
    real*8 :: sum
    integer :: i

    sum = 0
    do i = 1, length
        sum = sum + data(i)
    end do

end function

通过引入一个精简的包装函数,我们可以稍微简化工作,该函数负责将数组长度传递给 Fortran。然后,外部方法声明如下

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl,
EntryPoint = "__interop_MOD_sum_array")]
public static extern double SumArray(double[] input, ref int inputLength);

public static double SumArray(double[] input)
{
    int length = input.Length;
    return SumArray(input, ref length);
}

修改数组

在很多情况下,可能需要修改 Fortran 中数组的内容并将结果传回 C#。一个例子可能是跟踪对象的坐标集,这些坐标经过旋转。以下 Fortran 代码接受一个可变长度的 3D 坐标列表,这些坐标存储在 3xn 的二维数组中。这些坐标会围绕 Z 轴旋转。结果会写回到包含坐标的向量中。

! Demonstrates passing an array and modifying its contents between Fortran and the calling language.
! Rotates the passed vectors by the specified angle in radians over the Z-axis.
subroutine rotate_vectors(vectors, vectorCount, angle)

    use iso_c_binding, only: c_int, c_double
    
    integer*4, intent(in) :: vectorCount
    real*8, intent(in) :: angle
    real*8, intent(inout) :: vectors(3, vectorCount)
    real*8 :: rotationMatrix(3, 3)
    integer :: i
    
    ! Setup rotation matrix for rotation over Z-axis by angle.
    data rotationMatrix / 0, 0, 0, 0, 0, 0, 0, 0, 0 /
    rotationMatrix(1,1) = cos(angle)
    rotationMatrix(1,2) = -sin(angle)
    rotationMatrix(2,1) = sin(angle)
    rotationMatrix(2,2) = cos(angle)
    rotationMatrix(3,3) = 1
    
    ! Perform the rotation on each vector. Note the switched array indices
    ! because Fortran stores arrays column oriented, while C and .NET use row oriented.
    do i = 1, vectorCount
        vectors(:,i) = matmul(vectors(:,i), rotationMatrix)
    end do
    
end subroutine

为了能够从 .NET 世界使用上述代码,我们需要我们通常的外部方法声明

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_rotate_vectors")]
public static extern double RotateVectors([In, Out] double[,] vectors, 
ref int vectorCount, ref double angle);

唯一真正新增的是修饰向量参数的 [In, Out] 属性。InOut 属性指示 .NET 框架不仅需要将内存中的向量数组复制到非托管世界,还需要将其复制回托管世界。这个复制过程称为封送。细心的读者现在可能会注意到这很奇怪;因为我们传递的是向量数组的指针,所以任何更改都应该直接写入向量数组的内存块。不幸的是,由于封送的工作方式(在这种简单的双精度数组的情况下,很可能只传递指针),这并非总是如此。

传递数据结构

在更复杂的情况下,您可能希望使用结构体或类将数据传递给 Fortran 程序。由于 C# 和许多其他 .NET 语言面向对象的特性,这是一种非常自然的传递数据的方式。

Fortran 也有复杂数据类型的概念,因此完全可以通过结构体或类的形式将数据传递给 Fortran。但一如既往,有一些细节需要处理。在深入举例之前,这里简要讨论所有相关问题。

.NET 类或结构体

首先,您需要决定是要将数据作为结构体还是类传递。在 .NET 世界中,在此上下文中的关键区别在于 struct 是值类型,而类是引用类型。这意味着包含类实例的变量包含指向存储类实例的内存位置的引用,而包含 struct 实例的变量则包含 struct 的实际数据。

更多信息可以在 这里找到。

需要牢记这个关键区别,因为 Fortran 默认期望参数按引用传递,因此您需要确保正确执行参数传递。

数据顺序

第二个问题是结构体或类中的数据顺序。除非您采取特殊措施,否则无法保证数据结构的成员字段会按声明的顺序存储。以以下结构为例

public struct FortranInteropStruct
{
    public int Id;
    public double[] Values;
}

.NET 运行时没有任何理由不允许先存储双精度数组,然后再存储 Id 整数,而不是相反。对于 Fortran 复杂数据类型,情况也是如此。虽然情况可能不像每个编译器都会完全打乱您的数据类型顺序那么糟糕,但数据字段顺序是需要确保的事情。

在 C# 中,这可以通过 StructLayout 属性来实现,该属性允许指定数据结构的布局方式。它既适用于类也适用于 struct。通过传递 LayoutKind.Sequential,您可以指示运行时按照声明的顺序在内存中布局数据

[StructLayout(LayoutKind.Sequential)]
public struct FortranInteropStruct
{
    public int Id;
    public double[] Values;
}

在 Fortran 中,可以使用 sequential 关键字,它类似于 StructLayoutAttributeLayoutKind.Sequential 参数

type :: struct_name
    sequential
    [members]
end type

更多信息可以在 这里找到。

数据对齐

在向前和向后传递结构体时,最后需要注意的一件事是数据对齐。出于各种原因,通常将字段对齐到 4 或 8 字节边界是有益的。因此,例如,一个 short(16 位)整数字段只占用两个字节,下一个字段将只存储在 6 个字节之后。虽然我们不会详细介绍这样做的原因,但这需要注意。我想您能想象,如果 .NET 端和 Fortran 端的数据对齐不同,您在两者之间传递数据时将找不到您的值。

对于 Fortran 编译器,有时可以指定数据对齐。对于 GNU Fortran 编译器,我找不到该选项,因此我假设它是 8 字节。这在现代处理器上通常使用的对齐步长。Intel Fortran 编译器允许您显式指定所需的对齐。

在 .NET 端,我们可以使用 StructLayoutAttributePack 属性来指定数据对齐。为我们的示例 struct 将数据对齐设置为 8 字节如下

[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct FortranInteropStruct
{
    public int Id;
    public double[] Values;
}

封送处理

我们在数组部分已简要介绍过这个主题。封送是指管理托管代码和非托管代码之间数据传递的过程。到目前为止,我们只处理了在托管和非托管代码中具有相同表示形式的数据类型。例如,.NET 中的整数是 32 位长,这在 Fortran 世界中也没有不同。但对于更复杂的数据类型,如类,这些假设可能不再适用。String 是另一个例子;在 .NET 世界中,所有 string 都是 16 位字符元素的数组。但许多平台使用 8 位字符元素。

另一个问题是托管内存的性质。.NET 框架的垃圾回收器可能随时移动内存块,如果指向这些内存块的指针已传递给本地代码,可能会导致问题。.NET 封送功能可以通过将数据复制到固定内存块或固定托管内存块本身来解决此问题。

我不会在这篇文章中深入探讨封送的各个方面,但当您看到 InOutMarshalAs 等属性时,您应该知道它们用于控制数据在 .NET 世界和 Fortran 世界之间如何封送。

更多信息可以在 这里找到。

嵌入式数组

Fortran 默认使用嵌入式数组在其数据结构中。这意味着数组的值直接在结构体内部按顺序排列,而不是包含指向数组的指针。这与 .NET 世界中的工作方式相反。幸运的是,我们可以使用封送来管理这种差异,方法是在 C# 中用 MarshalAs 属性修饰数组并指定 UnmanagedType.ByValArray

[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct FortranInteropStruct
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] Values;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
    public char[] Name;
}

传递结构体

综合运用上述所有方法,就可以从 .NET 无缝使用以下 Fortran 代码。假设我们有一个简单的 Fortran 函数,它接受一个结构体并返回结构体中固定长度数组的元素之和

type :: interop_struct

    ! Fix the layout to sequential by using the sequence keyword.
    sequence

    integer :: id
    real*8 :: values(10)
    character*16 :: name

end type

! Demonstrates passing a structure / derived type to Fortran.
! Returns the sum of the values in the structure.
function pass_structure(data, extra_value) result(sum)

    type(interop_struct), intent(in) :: data
    real*8, intent(in) :: extra_value
    real*8 :: sum
    integer :: i

    write(*,*), "Passed an extra value of: ",extra_value

    sum = 0
    do i = 1, 10
        write (*,*), "Adding ",data%values(i)," 
        to ",sum," equals",(sum_intermediate + data%values(i))
        sum = sum + data%values(i)
    end do

    write(*,*), "Final sum ",sum

    return

end function

在 C# 中,我们定义一个匹配的结构体和匹配的外部方法声明

/// <summary>
/// Structure used to demonstrate marshalling complex data types between .NET and Fortran.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct FortranInteropStruct
{
    public int Id;

    /// <summary>
    /// Fixed length array holding double values.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] Values;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
    public char[] Name;
}
[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_pass_structure", CharSet = CharSet.Ansi)]
public static extern double PassStruct(ref FortranInteropStruct data, ref double extra_value);

也许在这个示例中真正需要注意的就是需要处理的固定大小数组。就像直接传递固定大小数组一样,我们需要指示 .NET 框架正确地封送这些数组。这可以通过用 MarshalAs 属性修饰数组来实现。此外,不要忘记嵌入式数组。

传递类

现在从 .NET 向 Fortran 传递类非常简单,唯一的关键区别是省略了 ref 声明。请注意,我们使用了与演示传递结构体时完全相同的 Fortran 函数来传递类。

/// <summary>
/// Class used to demonstrate marshalling complex data types between .NET and Fortran.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public class FortranInteropClass
{

    internal const int ValuesSize = 10;
    internal const int NameSize = 16;

    public int Id;

    /// <summary>
    /// Fixed length array holding double values.
    /// </summary>
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = ValuesSize)]
    public double[] Values;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = NameSize)]
    public char[] Name;

    public FortranInteropClass()
    {

        Values = new double[ValuesSize];
        Name = new char[NameSize];
    }
}

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_pass_structure", CharSet = CharSet.Ansi)]
public static extern double PassClass(FortranInteropClass data, ref double extra_value);

接收数据结构

理所当然,您也希望能够从 Fortran 接收数据结构。到目前为止,您应该能够猜到,与前两个示例相比,关键区别在于封送指令。通过用 InOut 属性修饰传递结构体的参数,我们确保在 Fortran 中对结构体所做的任何更改都会被复制回 .NET 世界。例如,以下 Fortran 代码通过给定量修改结构体中的值

! Demonstrates passing a structure / derived type back and forward between Fortran and the calling language.
! Increments all values in the struct by the specified amount.
subroutine modify_structure(data, change)

    type(interop_struct), intent(inout) :: data
    real*8, intent(in) :: change

    data%id = 123
    data%values(:) = data%values(:) + change

end subroutine

匹配的 C# 外部方法声明如下

[DllImport("Interop.dll", CallingConvention = CallingConvention.Cdecl, 
EntryPoint = "__interop_MOD_modify_structure", CharSet = CharSet.Ansi)]
public static extern void ModifyStruct([In, Out] ref FortranInteropStruct data, ref double change);

Using the Code

那么这一切真的有效吗?是的,它实际上效果很好。本文附带了示例代码,展示了所有这些技术。其中每段代码都作为一个单元测试,用于验证其正确的功能。

本系列的第一篇文章 中,已经相当广泛地介绍了编译和运行 Fortran 代码的过程。如果您对如何处理示例感到困惑,或者遇到问题,我恳请您阅读第一篇文章。

结论

在本文中,我们演示了如何传递和接收简单值、数组和结构体。这样做可以有效地利用 .NET 世界中的 Fortran 代码。许多使 C# 和 Fortran 代码互操作的方面也适用于使其他语言互操作。

在下一系列文章中,我们将讨论如何处理 string 以及如何在 Fortran 和 C# 之间传递回调函数。

历史

  • 2016 年 5 月 13 日:初始版本
© . All rights reserved.