我正在使用一个遗留的Fortran库,该库需要一个字符标量PATH
作为子程序的参数。最初的界面是:
SUBROUTINE MINIMAL(VAR1, ..., PATH)
CHARACTER (LEN=4096) PATH
...
我需要能够从C++调用它,所以我做了以下更改:
SUBROUTINE MINIMAL(VAR1, ..., PATH) &
BIND (C, NAME="minimal_f")
USE ISO_C_BINDING, ONLY: C_CHAR, C_NULL_CHAR
CHARACTER (KIND=C_CHAR, LEN=1), DIMENSION(4096), INTENT(IN) :: PATH
CHARACTER (LEN=4096):: new_path
! Converting C char array to Fortran CHARACTER.
new_path = " "
loop_string: do i=1, 4096
if ( PATH (i) == c_null_char ) then
exit loop_string
else
new_path (i:i) = PATH (i)
end if
end do loop_string
按照这个答案。这可以将C风格的char数组转换为Fortran标量数组,但有两个问题:
- 此代码位于关键路径上,因此每次在答案相同时进行相同的转换效率很低
- 我强烈希望不必编辑遗留代码
我试过:
- 只是直接接受带有ISO C绑定的
CHARACTER (LENGTH=4096) :: new_path
,但我得到了以下编译器错误:Error: Character argument 'new_path' at (1) must be length 1 because procedure 'minimal' is BIND(C)
这个答案和我读到的其他答案表明,ISO C绑定似乎限制了我可以作为参数传递给函数的内容,尽管我还没有找到任何官方文档 - 这个答案给出了另一种转换C风格字符串的算法转换为C代码中的Fortran风格等效程序,并在不使用ISO C绑定的情况下将其传递给Fortran子例程。(此函数建议使用类似的算法)。这似乎正是我想要的,但我在没有绑定的情况下出现了链接器错误:
Undefined symbols for architecture x86_64:
"_minimal", referenced from:
C++端函数声明:
extern "C" {
double minimal(int* var1, ..., const char* path);
}
这表明我的编译器(gcc)在extern
块中时会在函数名前面加下划线。但是,gfortran不允许我将子例程命名为_minimal
,因此链接器找不到符号_minimal
。(前面提到的链接建议在C端函数名的末尾添加一个下划线,但由于前导下划线,这也不起作用。)
我想在C++代码中将C风格的字符串处理成Fortran风格的字符标量一次,并能够将其传递到原始接口。有什么想法吗?
Fortran 2018允许可互操作过程具有假定长度的字符伪参数,从而放宽了此类伪参数长度必须为1的限制。
因此,我们可以将Fortran过程编写为
subroutine minimal(path) bind(c)
use, intrinsic :: iso_c_binding, only : c_char
character(*,c_char), intent(in) :: path
...
end subroutine minimal
继续我们的生活,我们知道我们还通过使用假定长度标量而不是显式长度标量改进了Fortran代码。否";Fortran端"需要此伪字符的副本。
这个故事的可悲之处在于,伪参数path
不能与char
互操作。因此,C(或C++)函数的形式参数不是char *
,而是CFI_cdesc_t *
。对于(C)示例:
#include "ISO_Fortran_binding.h"
#include "string.h"
void minimal(CFI_cdesc_t *);
int main(int argc, char *argv[]) {
/* Fortran argument will be a scalar (rank 0) */
CFI_CDESC_T(0) fpath;
CFI_rank_t rank = 0;
char path[46] = "afile.txt";
CFI_establish((CFI_cdesc_t *)&fpath, path, CFI_attribute_other,
CFI_type_char, strlen(path)*sizeof(char), rank, NULL);
minimal((CFI_cdesc_t *)&fpath);
return 0;
}
一个C++的例子也是类似的。
故事中值得注意的一点是,您需要一个Fortran编译器来实现Fortran 2018的这一部分。GCC 11没有。
IanH的回答引起了人们对一种完全避免修改原始Fortran子程序的方法的注意。当然,有时避免任何变化都是好的(稍微重复IanH所说的):
- 使用
bind(c)
意味着当通过Fortran本身调用修改后的子例程时,现在总是需要一个显式接口。也许代码的某些部分将其与隐式接口一起使用 - 原件已经过测试(或者没有测试),你不想破坏任何东西
- 您不希望潜在地将参数从默认类型更改为可互操作类型(如果它们确实不同)
- 确实需要显式长度伪参数
- 如果不需要,您只是不想修改它
其中任何一个都是很好的论据,因此本着这种精神,我将使用瘦包装器添加到C示例中。
Fortran:
subroutine minimal_wrap(path) bind(c, name='minimal')
use, intrinsic :: iso_c_binding, only : c_char
character(*,c_char), intent(in) :: path
call minimal(path)
end subroutine minimal_wrap
subroutine minimal(path)
character(4096) path
print*, trim(path)
end subroutine minimal
C:
#include "ISO_Fortran_binding.h"
#include "string.h"
void minimal(CFI_cdesc_t *);
static const int pathlength=4096;
int main(int argc, char *argv[]) {
/* Fortran argument will be a scalar (rank 0) */
CFI_CDESC_T(0) fpath;
CFI_rank_t rank = 0;
char path[pathlength];
/* Set path as desired. Recall that it shouldn't be null-terminated
for Fortran */
CFI_establish((CFI_cdesc_t *)&fpath, path, CFI_attribute_other,
CFI_type_char, pathlength*sizeof(char), rank, NULL);
minimal((CFI_cdesc_t *)&fpath);
return 0;
}
使用容器的C++可以说会更好。
回想一下,这让C端负责确保数组足够长(就像在纯Fortran调用中一样)。
同样,如果你需要对默认字符和该副本的可互操作字符的差异保持稳健(如IanH的回答),你可以根据需要将这些相同的技巧应用于复制(或者你可以通过条件编译和配置时间检查来做到这一点)。然而,到目前为止,您还可以假设始终复制或使用数组参数。
问题标题的答案通常是一个std::string
对象,用空格填充到相关的固定Fortran CHARACTER标量长度。C++端可以使用替代存储对象(std::vector<char>
或C样式的字符数组),但方法类似。
(如果Fortran代码使用的是假定长度的字符参数,而不是固定长度的字符,则不需要填充。是否可能进行此更改取决于MINIMAL子例程的详细信息。固定长度字符变量通常是一种时代错误-这个答案不主张在新代码中使用它们。)
在Fortran方面,您可以编写一个C++可以调用的瘦包装器,它使用序列和指针关联来避免复制字符串数据,适用于当今典型的C++/Fortran平台。如果可互操作的字符类型与传统Fortran过程的字符类型不同,则复制(或修改传统Fortran代码)是不可避免的。下面的示例代码对这种情况是稳健的,但我预计需要该代码路径的平台很少。
对于默认字符和C_CHAR可互操作的字符参数,序列关联允许数组伪参数与实际参数指定的字符序列相关联。这有效地允许字符标量和具有不同长度的数组之间的关联。
(不要将ISO_C_BINDING内部模块与BIND(C)过程后缀混淆。BIND(C)从根本上改变了过程的接口,以启用C和Fortran之间的调用-ISO_C_BINDING只是一个模块,它具有一些方便的类型、常量和用于此类调用的过程。)
示例C++:
#include <string>
#include <cassert>
const int path_length = 4096;
extern "C" int legacy_cintf(char* array);
int main()
{
std::string some_long_text
= "It was the best of times, it was the worst of times, it was "
"the age of wisdom, it was the age of foolishness, it was the "
"epoch of belief, it was the epoch of incredulity, it was the "
"season of light, it was the season of darkness, it was the "
"spring of hope, it was the winter of despair.";
assert(some_long_text.size() < path_length);
std::string path = std::string(path_length, ' ');
path.replace(0, some_long_text.size(), some_long_text);
legacy_cintf(&path[0]);
return 0;
}
示例Fortran:
MODULE m
IMPLICIT NONE
CONTAINS
SUBROUTINE legacy_cintf(array) BIND(C, NAME='legacy_cintf')
USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_INT, C_CHAR
CHARACTER(LEN=1,KIND=C_CHAR), TARGET :: array(4096)
CHARACTER(LEN=SIZE(array)), POINTER :: scalar
LOGICAL :: copy_required
copy_required = C_CHAR /= KIND(scalar)
IF (copy_required) THEN
ALLOCATE(scalar)
CALL do_copy(array, scalar)
ELSE
CALL do_associate(array, scalar)
END IF
CALL LEGACY(scalar)
IF (copy_required) DEALLOCATE(scalar)
END SUBROUTINE legacy_cintf
SUBROUTINE do_associate(arg, scalar)
CHARACTER(*), INTENT(OUT), POINTER :: scalar
CHARACTER(LEN=LEN(scalar)), INTENT(IN), TARGET :: arg(1)
scalar => arg(1)
END SUBROUTINE
SUBROUTINE do_copy(arg, scalar)
USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_CHAR
CHARACTER(*), INTENT(OUT) :: scalar
CHARACTER(LEN=LEN(scalar), KIND=C_CHAR), INTENT(IN) :: arg(1)
scalar = arg(1)
END SUBROUTINE do_copy
END MODULE m
SUBROUTINE LEGACY(PATH)
CHARACTER(4096) :: PATH
PRINT *, TRIM(PATH)
END SUBROUTINE LEGACY