从C++拦截Fortran STOP

5
我为一个老的Fortran库准备了一个C++接口。
遗留库中的某些子程序遵循一种丑陋但可用的状态码约定来报告错误,我使用这些状态码从我的C++代码中抛出可读的异常:这非常有效。
另一方面,有时候即使条件是可以恢复的,这个遗留库也会调用STOP(终止程序)。
我想要在C++中“捕获”这个STOP,到目前为止我还没有成功。
以下代码很简单,但确切地代表了手头的问题:
Fortran遗留库fmodule.f90:
module fmodule
  use iso_c_binding
  contains
    subroutine fsub(x) bind(c, name="fsub")
      real(c_double) x
      if(x>=5) then 
         stop 'x >=5 : this kills the program'
      else
         print*, x
      end if
    end subroutine fsub    
end module fmodule

C++接口main.cpp

#include<iostream>

// prototype for the external Fortran subroutine
extern "C" {
  void fsub(double& x);  
}

int main() {  
  double x;
  while(std::cin >> x) {
    fsub(x);
  }
  return 0;
}

编译命令行(GCC 4.8.1 / OS X 10.7.4; $ 表示命令提示符):
$ gfortran -o libfmodule.so fmodule.f90 -shared  -fPIC -Wall
$ g++ main.cpp -L. -lfmodule -std=c++11

运行:

$ ./a.out 
1
   1.0000000000000000     
2
   2.0000000000000000     
3
   3.0000000000000000     
4
   4.0000000000000000     
5
STOP x >=5 : this kills the program

我该如何捕获STOP信号,然后请求另一个数字。请注意,我不想修改Fortran代码

我尝试过的方法:

  • std::atexit:一旦进入,就无法“回来”
  • std::signal:似乎无法捕获STOP信号

我猜这很难——STOP 的作用是终止进程。这与调用 exit 的 C/C++ 库没有什么不同。在这种情况下,你可能能够在 FORTRAN 运行时之前挂钩 STOP 调用(如果可能的话),但从本质上讲,你要处理的代码并不是按照库应有的行为编写的。修复 FORTRAN 库可能是一条更少痛苦且易于验证的路线。 - Joe
不可能的,你必须调整Fortran代码,或者劫持特定编译器用于STOP的运行时库调用。 - Vladimir F Героям слава
2
我看到它实际上是Fortran 90+。通过丑陋的遗留FORTRAN代码,人们通常会想到不同的东西。 - Vladimir F Героям слава
我真的,真的,真的不想碰Fortran代码。但似乎几乎是不可避免的... - Escualo
并不完全正确地说,一个atexit处理程序无法“返回”-请参见我的答案。 - Hristo Iliev
4个回答

11

通过拦截Fortran运行时对exit函数的调用,您可以解决问题。请参见下文。 a.out是使用您提供的代码和编译行创建的。

步骤1. 确定所调用的函数。启动gdb

$ gdb ./a.out
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-60.el6_4.1)
[...]
(gdb) break fsub
Breakpoint 1 at 0x400888
(gdb) run
Starting program: a.out 
5

Breakpoint 1, 0x00007ffff7dfc7e4 in fsub () from ./libfmodule.so
(gdb) step
Single stepping until exit from function fsub,
which has no line number information.
stop_string (string=0x7ffff7dfc8d8 "x >=5 : this kills the programfmodule.f90", len=30) at /usr/local/src/gcc-4.7.2/libgfortran/runtime/stop.c:67

所以调用了stop_string函数。我们需要知道这个函数对应哪个符号。

步骤2. 找到stop_string函数的确切名称。它必须在其中一个共享库中。

$ ldd ./a.out 
    linux-vdso.so.1 =>  (0x00007fff54095000)
    libfmodule.so => ./libfmodule.so (0x00007fa31ab7d000)
    libstdc++.so.6 => /usr/local/gcc/4.7.2/lib64/libstdc++.so.6 (0x00007fa31a875000)
    libm.so.6 => /lib64/libm.so.6 (0x0000003da4000000)
    libgcc_s.so.1 => /usr/local/gcc/4.7.2/lib64/libgcc_s.so.1 (0x00007fa31a643000)
    libc.so.6 => /lib64/libc.so.6 (0x0000003da3c00000)
    libgfortran.so.3 => /usr/local/gcc/4.7.2/lib64/libgfortran.so.3 (0x00007fa31a32f000)
    libquadmath.so.0 => /usr/local/gcc/4.7.2/lib64/libquadmath.so.0 (0x00007fa31a0fa000)
    /lib64/ld-linux-x86-64.so.2 (0x0000003da3800000)

我在(毫不意外地)Fortran运行时中找到了它。

$ readelf -s /usr/local/gcc/4.7.2/lib64/libgfortran.so.3|grep stop_string
  1121: 000000000001b320    63 FUNC    GLOBAL DEFAULT   11 _gfortran_stop_string@@GFORTRAN_1.0
  2417: 000000000001b320    63 FUNC    GLOBAL DEFAULT   11 _gfortran_stop_string

步骤3. 编写一个函数来替换该函数

我在源代码中查找该函数的精确签名(/usr/local/src/gcc-4.7.2/libgfortran/runtime/stop.c 参见 gdb 会话)

$ cat my_exit.c 
#define _GNU_SOURCE
#include <stdio.h>

void _gfortran_stop_string (const char *string, int len)
{
        printf("Let's keep on");
}

第四步。编译一个导出该符号的共享对象。

gcc -Wall -fPIC -c -o my_exit.o my_exit.c
gcc -shared -fPIC -Wl,-soname -Wl,libmy_exit.so -o libmy_exit.so my_exit.o

步骤5:使用LD_PRELOAD运行程序,以便我们的新函数优先于运行时中的函数。

$ LD_PRELOAD=./libmy_exit.so ./a.out 
1
   1.0000000000000000     
2
   2.0000000000000000     
3
   3.0000000000000000     
4
   4.0000000000000000     
5
Let's keep on   5.0000000000000000     
6
Let's keep on   6.0000000000000000     
7
Let's keep on   7.0000000000000000   

就是这样。


你觉得重新定义 STOP 为调用你的函数的 cpp 宏会更容易些吗? - Vladimir F Героям слава
甚至-cpp -DSTOP=! - Vladimir F Героям слава
不错。需要注意的是:FORTRAN代码可能会或可能不会在通常会STOP的良好状态下保留事物。@Arrieta:您需要检查调用的例程是否假定先前可能不完整的状态... - Joe
4
如果您使用gfortran -c -fdump-tree-all fmodule.f90编译Fortran代码,然后查看fmodule.f90.003t.original文件,“步骤1和2”将一起花费5秒钟: _gfortran_stop_string (&"x>=5:this kills the program"[1]{lb: 1 sz: 1},30);许多人都应该了解GCC语法树转储-在许多情况下,这些会非常有用。 - Hristo Iliev
3
请注意,您所做的操作会防止“STOP”停止子程序并使其继续执行。这可能导致代码后续出现各种糟糕的情况:越界访问数组、数值上溢/下溢等问题。 - Hristo Iliev
显示剩余4条评论

5

既然您想要的结果将导致代码不可移植,为什么不使用晦涩的长跳转机制来破坏退出机制:

#include<iostream>
#include<csetjmp>
#include<cstdlib>

// prototype for the external Fortran subroutine
extern "C" {
  void fsub(double* x);  
}

volatile bool please_dont_exit = false;
std::jmp_buf jenv;

static void my_exit_handler() {
  if (please_dont_exit) {
    std::cout << "But not yet!\n";
    // Re-register ourself
    std::atexit(my_exit_handler);
    longjmp(jenv, 1);
  }
}

void wrapped_fsub(double& x) {
  please_dont_stop = true;
  if (!setjmp(jenv)) {
    fsub(&x);
  }
  please_dont_stop = false;
}

int main() {
  std::atexit(my_exit_handler);  
  double x;
  while(std::cin >> x) {
    wrapped_fsub(x);
  }
  return 0;
}

调用longjmp会直接跳转到带有setjmp调用的代码行中间,而setjmp返回传递给longjmp的第二个参数。否则setjmp将返回0。示例输出(OS X 10.7.4,GCC 4.7.1):

$ ./a.out 
2
   2.0000000000000000     
6
STOP x >=5 : this kills the program
But not yet!
7
STOP x >=5 : this kills the program
But not yet!
4
   4.0000000000000000
^D     
$

不需要预加载任何库(在OS X上比在Linux上更涉及一些细节)。但是需要注意,退出处理程序的调用顺序与它们的注册顺序相反。因此,在my_exit_handler之后不要再注册其他退出处理程序。


这非常聪明。而且便携。让我来试试它。 - Escualo
@Arrieta,请注意,这种方式下 fsub 的堆栈未被正确卸载(因为 STOP 应该终止整个程序),这意味着例如本地可分配数组未被释放。你可能会遇到内存泄漏的问题。 - Hristo Iliev

1

结合使用自定义的_gfortran_stop_string函数和longjmp的两个答案,我认为在自定义函数内部引发异常,然后在主代码中捕获异常会很相似。因此,下面的代码就产生了:

main.cpp:

#include<iostream>

// prototype for the external Fortran subroutine
extern "C" {
  void fsub(double& x);  
}

int main() {  
  double x;
  while(std::cin >> x) {
    try { fsub(x); }
    catch (int rc) { std::cout << "Fortran stopped with rc = " << rc <<std::endl; }
  }
  return 0;
}

catch.cpp:

extern "C" {
    void _gfortran_stop_string (const char*, int);
}

void _gfortran_stop_string (const char *string, int len)
{
        throw 666;
}

然后,编译:
gfortran -c fmodule.f90
g++ -c catch.cpp
g++ main.cpp fmodule.o catch.o -lgfortran

运行中:
./a.out
2
   2.0000000000000000     
3
   3.0000000000000000     
5
Fortran stopped with rc = 666
6
Fortran stopped with rc = 666
2
   2.0000000000000000     
3
   3.0000000000000000     
^D

所以,看起来可以工作 :)


0

我建议在调用Fortran代码之前分叉您的进程,并在Fortran执行后退出0(编辑:如果STOP以零退出,则需要一个哨兵退出代码,有些笨拙但可以完成工作)。这样,每个Fortran调用都将以相同的方式完成:与停止时相同。或者,如果“STOP”确保出现错误,请在Fortran代码停止时抛出异常,并在Fortran执行“正常完成”时发送其他消息。

以下是一个示例,灵感来自您的代码,假设Fortran“STOP”是一个错误。

 int main() {  
   double x;
   pid_t pid;
   int   exit_code_normal = //some value that is different from all STOP exit code values
   while(std::cin >> x) {
     pid = fork();
     if(pid < 0) {
       // error with the fork handle appropriately
     } else if(pid == 0) {
       fsub(x);
       exit(exit_code_normal);
     } else {
       wait(&status);
       if(status != exit_code_normal)
          // throw your error message.
     }
   }
   return 0;
 }

退出代码可以是常量而不是变量。我认为这并不重要。

根据评论,如果结果保存在进程的内存中(而不是写入文件),则执行结果将会丢失。如果是这种情况,我可以想到3种可能性:

  • Fortran代码在调用过程中破坏了大量内存,并且让执行超出STOP可能首先不是一个好主意。
  • Fortran代码只需返回一些值(通过其参数,如果我的Fortran不太生疏),这可以通过共享内存空间轻松地传递回父级。
  • Fortran子例程的执行作用于外部系统(例如:写入文件),不需要返回值。

在第三种情况下,我上面的解决方案就像原样工作。我更喜欢它而不是其他建议的解决方案,主要原因是:1)您不必确保构建过程得到正确维护2)Fortran“STOP”仍然按预期运行,3)它只需要非常少的代码行,所有“Fortran STOP workaround”逻辑都位于一个单一的位置。因此,在长期维护方面,我更喜欢它。

在第二种情况下,我的代码需要进行小的修改,但仍然保持了上述优点,代价是最小化的复杂性。
在第一种情况下,无论如何都必须对Fortran代码进行干扰。

2
这很聪明,但是如果fsub修改全局状态或返回值怎么办?由于写时复制内存映射,父进程将无法看到子进程所做的更改。 - Hristo Iliev
优秀的评论。如果FORTRAN代码是不可重入的,或者返回值很难从父级获取,那么这可能会变得非常复杂。 - Sebastien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接