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

Seb*_*rek 6 c c++ fortran language-interoperability

我正在使用一个旧的 Fortran 库,它需要一个字符标量PATH作为子例程的参数。原来的界面是:

SUBROUTINE MINIMAL(VAR1, ..., PATH)

CHARACTER (LEN=4096) PATH

...
Run Code Online (Sandbox Code Playgroud)

我需要能够从 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
Run Code Online (Sandbox Code Playgroud)

根据这个答案。这可以将 C 风格的 char 数组转换为其 Fortran 标量数组,但存在两个问题:

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

我努力了:

  • 只是接受CHARACTER (LENGTH=4096) :: new_path直接与 ISO C 绑定,但我收到以下编译器错误: Error: Character argument 'new_path' at (1) must be length 1 because procedure 'minimal' is BIND(C) 这个答案和我读过的其他答案表明 ISO C 绑定似乎限制了我可以作为参数传递给函数的内容,尽管我没有尚未找到任何官方文档。
  • 这个答案提供了另一种算法,可以将 C 风格的字符串转换为 C 代码中的 Fortran 风格的等效字符串,并将其传递给 Fortran 子例程,而不使用 ISO C 绑定。(此函数建议类似的算法)。这似乎正是我想要的,但在没有绑定的情况下出现链接器错误:
Undefined symbols for architecture x86_64:
  "_minimal", referenced from:
Run Code Online (Sandbox Code Playgroud)

C++端函数声明:

extern "C" { 
    double minimal(int* var1, ..., const char* path);
}
Run Code Online (Sandbox Code Playgroud)

这表明我的编译器 (gcc) 在extern块中时在函数名称前面添加了下划线。然而,gfortran 不允许我命名子例程_minimal,因此链接器无法找到符号_minimal。(上述链接建议在 C 端函数名称末尾添加下划线,但这也不起作用,因为前导下划线。)

我想在我的 C++ 代码中将 C 风格的字符串处理为 Fortran 风格的字符标量,并能够将其传递到原始接口中。有任何想法吗?

fra*_*lus 4

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
Run Code Online (Sandbox Code Playgroud)

并继续我们的生活,知道我们还通过使用假定长度标量而不是显式长度标量改进了 Fortran 代码。不需要此角色虚拟的“Fortran 端”副本。

这个故事的可悲之处在于虚拟参数path不能与char. 因此,C(或 C++)函数的形式参数char *必须是 ,而不是CFI_cdesc_t *。以(C)为例:

subroutine minimal(path) bind(c)
  use, intrinsic :: iso_c_binding, only : c_char
  character(*,c_char), intent(in) :: path
  ...
end subroutine minimal
Run Code Online (Sandbox Code Playgroud)

C++ 示例将是类似的。

这个故事值得注意的部分是,您需要一个 Fortran 编译器来实现 Fortran 2018 的这一部分。GCC 11 不需要。


IanH 的回答引起了人们对一种完全避免修改原始 Fortran 子例程的方法的关注。当然,有时候避免任何改变是有好处的(稍微重复一下 IanH 所说的):

  • usingbind(c)意味着现在通过 Fortran 本身调用修改后的子例程时始终需要显式接口。也许代码的某些部分将其与隐式接口一起使用
  • 原件已经过测试(或没有测试),并且您不想破坏任何东西
  • 您不想将参数从默认类型更改为可互操作类型(如果它们确实不同)
  • 确实需要显式长度虚拟参数
  • 如果不需要的话你只是不想修改它

其中任何一个都可以成为一个很好的论据,因此本着这种精神,我将使用薄包装器添加到 C 示例中。

福特兰语言:

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
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

使用容器的 C++ 可以说会更好。

回想一下,这将责任放在 C 端,以确保数组足够长(就像在纯 Fortran 调用中一样)。

同样,如果您需要对该副本的默认字符和可互操作字符的差异保持鲁棒性(如 IanH 的答案),您可以应用这些相同的技巧来根据需要进行复制(或者您可以通过条件编译和配置时检查来做到这一点)。然而,此时,您也可以假设始终复制或使用数组参数。