如何在C++数据结构中存储Fortran风格的长字符标量



我正在使用一个遗留的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标量数组,但有两个问题:

  1. 此代码位于关键路径上,因此每次在答案相同时进行相同的转换效率很低
  2. 我强烈希望不必编辑遗留代码

我试过:

  • 只是直接接受带有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

最新更新