||
Fortran与C#混合编程
Fortran语言是世界上最早出现的、比较底层的计算机数值计算高级程序设计语言,较高层的程序设计语言,它具有实现精度高、计算速度快的功能特点,且其自身就拥有非常强大的程序集和计算类的数据结构,因此广泛应用于数值、科学和工程计算领域。但是Fortran语言的图形界面开发功能较弱,不适合开发独立的桌面或Web应用程序。C# 是最近几年微软推出的新一代面向对象的界面开发语言,它以强大的.NET底层框架为基础,拥有十分强大的图形化界面开发平台。C# 程序设计以C 和C++ 入门,在保留C++ 设计灵活性的基础上,加入了VB 的快速开发,因此十分适合图形界面的交互系统快速开发。然而,由于.NET采用的是托管机制,使得C#语言的计算速度、精度以及执行效率相对来说不是很理想。
因此,本文设想:能否在开发一个系统的时候将系统的工程计算部分和项目组织部分分开实现?将计算部分用数据精度高、计算速度快、执行效率高的Fortran语言实现,将项目组织部分用快速、高效的C#图形化用户设计语言。如此,在开发交互式解释系统软件时,可以采用C#与Fortran混合编程,这样既可以发挥C#的高效开发特点,又使得现有的经典Fortran计算程序可以得到充分利用。本文主要从混合编程的实现方面阐述了一些常用的方法和要点。
混合编程,是指利用两种或两种以上的程序设计语言组合起来的程序设计开发,彼此互相调用,传递参数,共享数据结构或数据信息,从而形成一个统一的程序系统的过程。利用Fortran语言和C#语言进行混合编程是为了能够更充分地发挥两种语言各自的特点,其基本思路如下:
运用Fortran 语言编写动态链接库( DLL ),在DLL 中提供计算的函数接口,然后在C# 中调用该DLL 中计算部分的函数,实现计算过程。这里需要注意的是,由于我们使用的是Fortran 编译器,生成的DLL 属于第三方非托管DLL ,因此无法直接在程序中添加DLL 的引用。
1. 混合编程的基本步骤
1.1 创建Fortran 动态链接库DLL
STEP1:进入到FORTRAN集成开发环境下,依次打开File | New | Fortran Dynamic Link Library,为新的动态库命名如: FORTRAN.DLL
STEP2:使用Ctrl + N快捷方式,添加文件Free Format的*.f90子程序作为当前工作空间。
STEP3:Fortran建立动态链接库是使用子函数形式的,我们推荐是用SUBROUTINE而避免使用FUNCTION.同时要记得使用编译为DLL的注释性命名:
!DEC$ ATTRIBUTES DLLEXPORT :: FORDLL
STEP4:当完成DLL的程序设计以后,编译生成DLL文件。进入工程文件中的Bin/Debug
目录,便能找到该DLL库文件。
1.2 C# 调用Fortran DLL的过程
准备工作:
(1) 将Fortran生成的DLL文件拷贝至C#工程文件的bin/Debug目录下,目的是确保可执行程序(exe文件)与库文件(dll文件)在同一文件夹中,便于Windows自动查找DLL文件。当然这里除了手动拷贝DLL到C#的工程目录下外,还可以使用脚本程序让开发工具来完成。
(2) 在进行Dll Import连接之前,需要在C#中增加对动态连接库操作的类的引用:
Using System.Runtime.Interopservices;
完成相关的连接设置就可以在C#程序中使用Fortran DLL中的子例程了。C#调用动态链接库的参数设置是使用Dll Import属性来实现。然后使用一个实例来装载传递过来的子例程和相应的参数。
Example:
[DllImport("FORTRAN.DLL", SetLastError = True, CharSet = CharSet.Unicode, CallingConvention= CallingConvention.StdCall)] //通过DLL Import属性来设置调用DLL文件的参数。
public staticextern void FORDLL() //使用一个具体的实例来装载从Fortran编译的DLL文件传递过来的子例程和相应的参数。
DllImport的命名参数:
l CallingConvention指示入口点的调用约定。如果未有指定,则使用默认值: CallingConvention.Winapi
l CharSet指示用在入口点中字符集,默认为CharSet.Auto
l EntryPoint给出DLL中入口点的函数名称。若未指定,则使用方法本身的名称。
l ExactSpelling指示EntryPoint是否必须与指示的入口点的拼写完全匹配。默认为False
l PreserveSig指示方法的签名被保留还是被转换。当签名被转换时,它被转换为一个具有HRESULT返回值和该返回该值的一个名为retval的附加输出参数的签名。默认值为True。
l SetLastError指示方法是否保留Win32"上一错误",默认为False。
FORDLL修饰符说明:
l public用来说明这个函数是公用的,可以在程序中的其它地方访问它;
l static则表示这个函数是静态的,即C#在调用的时候不会传递参数外的其它信息;
l extern则表示这个函数由程序以外的模块实现;
l void代表函数的返回类型,这个视情况而定。
2. 混合编程的关键技术
混合编程时,要注意调用程序与被调用程序遵守相同的规则,包括语言约定的一致性和数据处理的相容性等接口问题。
2.1语言约定一致
<1> 函数名命名约定。相互匹配的标识符在编程过程中应处理成一致的,因为不同语言的编译器智能机械的按本语言的命名约定进行处理。C# 语言的符号名是区分大小写的,Fortran语言中不存在符合名大小写的问题,在Fortran 编译器中,默认的导出函数名是纯大写形式,两者如果处理不一致则导致程序连接失败。
而在 C# 中调用Fortran Dll 时,必须指定与Fortran中的函数名一致。在Fortran代码中的解决办法是:
①Fortran的缺省方式使符号名在OBJ文件中变成大写,如果在C#中调用一个使用Fortran缺省的子例程时,在C#中需用一个纯大写的函数名称来调用;
DOUBLE PRECISIONFUNCTIONADD (A, B)
!DEC$ ATTRIBUTES DLLEXPORT:: ADD
DOUBLE PRECISION A, B
ADD =A + B
END
纯大写——对应的C#声明为:
[DllImport("MathDll")]
privatestaticexterndoubleADD (double A, double B);
②如果想在C#中使用小写声明Fortran中的子例程,则应该在Fortran程序中需用C和STDCALL属性将所有名称转换为纯小写的形式;
③若是在C#中调用一个例程以大小写混合形式出现的时候,需要使用Fortran的ALIAS属性来解决混合形式之间的命名冲突。
Double PrecisionFunction ADD (A, B)
! DEC$ATTRIBUTES DLLEXPORT :: ADD
! DEC$ATTRIBUTES ALIAS: 'Add' :: Add
Double Precision A, B
Add = A + B
End
大小写混合——对应的C#声明为:
[DllImport("MathDll")]
privatestaticexterndouble Add (doubleA, double B);
④ C#中提供的解决方案是,通过使用Dlllmport的EntryPoint属性,指定Fortran的导出函数名。例如:
DOUBLE PRECISIONFUNCTION ADD (A, B)
!DEC$ATTRIBUTES DLLEXPORT :: ADD
DOUBLE PRECISION A, B
ADD = A + B
END
对应的C#声明为:
[DllImport ("MathDll",EntryPoint = " ADD ")]
privatestaticexterndouble ADD(double A, double B);
<2> 堆栈管理约定。C#语言在windows平台上的调用模式默认为StdCall模式,既由被调用方清理堆栈。而Fortran语言则默认由调用方清除。所有涉及堆栈这样一种数据结构的参数,调用例程(C#)和被调用例程(Fortran)必须统一调用双方的堆栈清除方式才能保证2种语言间的正常函数调用。因此,堆栈管理约定包括:在调用过程中子例程接受参数的数目和顺序,调用完成后由哪一方来清理堆栈等。这一约定在Fortran语言或C#语言中均可以采取措施进行统一。
1)在Fortran语言中可以通过编译指令“!DEC$”后的可选项“C”或“STDCALL”参数来实现:
l STDCALL模式指定由被调用方清除堆栈。
l C模式声明由主调用函数清除堆栈(但在传递数组和字符串参数时不能用此方法)。
2)如果在C#语言内做改动,则需要在DllImport属性中设置CallingConvention字段的值为Cdecl(表示由主调用方清理堆栈)或StdCall(表示由被调用方清理堆栈)。
[DllImport("FileName.dll", CallingConvention = CallingConvention.StdCall)]
[DllImport("FileName.dll",CallingConvention = CallingConvention.Cdecl)]
堆栈清理方式 程序语言 | 调用方清理 | 被调用方清理 |
C# | Cdecl模式 | StdCall模式(C#默认) |
FORTRAN | C模式 | STDCALL模式 |
<3>参数传递约定。只有以同样的方式发送和接收参数,才能获得正确的数据传送和正确的程序结果。常见的参数传递为值传递和引用传递两种。C#默认的是值传递,而Fortran默认的是引用传递,因此在混合编程过程中应注意保持传递方式的一致性。
1) 若统一为引用传递,在Fortran中无需做任何修改,因为Fortran默认的就是引用传递类型;而只需将C#的参数类型定义为引用类型,此时需使用ref关键字。
public static extern voidFORDLL(ref int m); //通过ref将整形参数m被定义为引用传递
2) 若统一为值传递类型,在C#中无需做任何修改,因为C#中默认的就是值传递类型;而只需将Fortran的参数定义为值类型,此时使用VALUE关键字。
!DEC$ ATTRIBUTES VALUE:: m !使用VALUE将m定义为值传递方式
若传递过程中既有值类型,也有引用类型,则为上面的两种综合使用,Fortran中使用引用为REFERENCE。
!DEC$ ATTRIBUTES REFERENCE:: m !使用REFERENCEE将m定义为引用传递方式
2.2数据类型一致
在Fortran中常用的数据参数类型有:REAL,INTEGER,DOUBLEPRECISION。
REAL:表示浮点数据类型,即小数,等价于C#的float。
INTEGER:表示整数类型,相当于C#的int数据类型。
DOUBLEPRECISION: 表示双精度数据类型,相当于C#的double数据类型。
在C#调用Fortran DLL是必须保证参数的一致性,例如在Fortran中变量定义的是REAL类型,而我们传入的是Double,那么就会出现计算错误。
数据传递也就是涉及基本的数值传递和字符串的传递,其中数值传递又包括单值型数据和数组型的数据。
<1>数值传递。满足好引用类型就基本可以实现,但是在传递数组的时候要特别注意:C#的数组是从0开始记录的,而Fortran则是从1开始记录;另外,C#采用的是按行存放,而Fortran则为按列存放。在传递时应详细考虑数组问题。
例如在Fortran中的A[2][3]数组,在C#中就应为A[1,2]。
并且数组传递还应注意,只需要传递数组的首地址即可,DLL需要的是数组的起始位置。
int[] bb = new int[5];
FORDLL(ref bb[0]);
<2>字符串传递。传递和返回字符串是混合编程中最为复杂,也是需要考虑问题最多的部分。C#和Fortran的字符串传递同样麻烦,首先,C#中的字符串是双字节的Unicode类型,而Fortran中默认的是单字节的Ansi类型。这样产生的问题是C#会自动把Fortran的每两个字符合并为一个字符,Fortran中的128个字符的字符串变成了C#中的64个字符的垃圾。另外,C#和Fortran关于字符串的表示方式也有所不同:C#的字符串表示方式与C语言相同,使用\0表示字符串的结束;Fortran中则采用在最右端添加空格表示,并在最右端使用一个隐藏的参数表示实际的长度,因此要正确的传递字符串,应该解决如何正确表达字符串长度的问题。
解决方案1:
原理很简单,就是避免直接的进行不同类型的字符串传递,换用整型数组传递。C#中的字符串传向FORTRAN的具体操作流程如下:
step1:在C#中将字符串分割为字符数组;
step2:再将字符数组转为ASCII码数组(0~128的整数),然后传递给FORTRAN;
step3:在FORTRAN中利用CHAR()函数将ASCII码还原为字符串即可。
NOTE: FORTRAN传向C#也是可以采用相同的思路进行
---------------------------------------------------------------------
// C#中的字符串分割,并转存为ASCII码数组
String c = "abcdefg";
ASCIIEncoding ascii = newASCIIEncoding();
int[] num = newint[c.Length];
for (int i = 0; i < c.Length; i++)
{
num[i] = (int)ascii.GetBytes(c)[i]; //通过ASCIIEncoding类的对象调用GetBytes方法将字符串转变成字符,然后将字符转变成8位的Byte类型,再将Byte类型转变成int类型。
}
int m = c.Length;
S(ref num[0], ref m);//在C#中使用ref关键字,表示参数传递时使用引用类型
---------------------------------------------------------------------
//C#调用DLL声明及操作
[DllImport("DLLTEST.dll",SetLastError = true, CharSet = CharSet.Unicode,
CallingConvention = CallingConvention.StdCall)]//DLL程序在C#代码中的入口点
publicstaticexternvoid S(refint ka, refint m); //使用一个具体实例S 来装载传递过来的子例程和相应的参数。
//参数说明:ka为ASCII码数组名; m为ka数组大小(即字符串长度)
----------------------------------------------------------------------
!FORTRAN中还原字符串的操作
SUBROUTINE S(ka, m)
!DEC$ ATTRIBUTESDLLEXPORT :: S
CHARACTER(m)::str
DIMENSION ka(m)
INTEGER i
INTEGER x, y
DO i=1, m
str(i : i) = char(ka(i))
END DO
END
-----------------------------------------------------------------------
解决方案2:
根据Compaq Visual Fortran Version 6.6随机帮助文件中的Programmer's Guide -> Creating Fortran DLLs & -> Programming withMixed Languages和Language Reference -> A to Z Reference -> A toB -> ATTRIBUTES等相关部分的内容做出的一个测试例程,用来说明C# 调用Fortran DLL过程中参数传入Fortran的实现(只涉及到了数值类型和字符及字符串类型,其它类型读者可以继续到帮助中阅读相关资料)。
! FortranDLL.f90文件的内容如下:
! 用来建立DLL项目
! 函数WTSTR(Str)用来建立Str文件并在该文件里记录Str字符串的内容
SUBROUTINE writestr(str)
!DEC$ ATTRIBUTESDLLEXPORT,ALIAS:'WTSTR':: Writestr
Character*(*) str
Open(1, file = str)
Write(1,*) str
END SUBROUTINE writestr
!函数Iadd2(A,B)用来就算两个整型数A、B的和并将结果输出到屏幕
SUBROUTINE iadd2(a, b)
!DEC$ ATTRIBUTES C, DLLEXPORT, ALIAS: 'Iadd2' :: Iadd2
INTEGER :: a, b
INTEGER :: sum
sum = a + b
WRITE(*,*) "The sum:",sum, "in DLL iadd2"
END SUBROUTINE iadd2
!上述Iadd2函数中用来C属性字段,是用来说明参数是按值来传送的,而在WTSTR函数中因为是
!用来传递字符类型的,所以不能用“C”。(If C or STDCALL is specified for a subprogram,
!arguments(exceptfor array sand characters)are passed by value.)
//C#中的调用代码如下:
//这部分代码将编译生成.EXE可执行程序,调用上面的DLL
using System;
using System.Runtime.InteropServices;
namespace CCallDll
{
class Program
{
[DllImport("dlltest.dll")]//WTSTR函数说明部分,注意此处字符串参数的传递
public static extern voidWTSTR(string str,int strlength);
[DllImport("dlltest.dll")]//Iadd2函数说明部分
//两种类型的参数传递涉及到在Fortran程序中的处理也不一样
public static extern void Iadd2(inta, int b);
//主程序入口
static void Main(string[] args)
{
//Delcare the String Variable. Notice: it'sUnicode which occupies 2 Bytes
string unicodeString="This.txt";
Console.WriteLine("Originalstring is:{0}",unicodeString);
Console.WriteLine("The lengthof the string:{0}",unicodeString.Length);
//Call the Function of WTSTR(,)此处增加了一个表示字符串长度的参数是因为
//Fortran、和C#存贮字符串的方法不一样(详情查阅相关资料,有很多介绍)
WTSTR(unicodeString, unicodeString.Length);
Iadd2(1000,10);
}
}
}
Archiver|手机版|科学网 ( 京ICP备07017567号-12 )
GMT+8, 2024-11-22 00:41
Powered by ScienceNet.cn
Copyright © 2007- 中国科学报社