Fortran查询并打印出函数或子程序名称

11

在Fortran中,是否可以查询当前所在函数或子程序的名称?即,在代码中替换 '???' 可以打印出屏幕上的'my_subroutine'。

subroutine my_subroutine()
   write(*,*) ???
end subroutine my_subroutine

我正在尝试使用文本编辑器的搜索和替换机制实现自定义调试器/分析器。编程查询我的代码位置将会很有帮助。


我可以建议您在调试时使用现有的调试器,例如GDB,并且为了识别要优化的代码,您可以从该调试器中进行某种堆栈采样。或者找到像pstack这样的实用程序。这是一种不同于测量时间的方法,我猜想您正在考虑这个。 - Mike Dunlavey
5个回答

12

你可以使用预处理器来打印文件名和行号。你可能想要利用预定义的预处理器符号__LINE____FILE__。以下是一个示例:

在头文件中定义预处理宏(以便在多个位置使用),称之为errormsg.h

#define ERRORMSG(msg) write(0,'("There was an error at ",I4," in file ",/,A,/,"Error message: ",A)') __LINE__,__FILE__,msg

然后,您可以将此头文件包含在程序、库或模块文件中,例如:

#include "errormsg.h"

program main 

  ERRORMSG("not really an error...")
  call foo()

end program


subroutine foo()

  ERRORMSG("not an error too!")

end subroutine

ERRORMSG("not really an error...") 看起来像是Fortran代码中的奇怪语法,但它会使用宏定义被C预处理器替换。因此,编译时它看起来是这样:

write(0,'("There was an error at ",I4," in file ",/,A,/,"Error message: ",A)') __LINE__,__FILE__,"not really an error"

对于我的ERRORMSG宏,我选择使用0文件单元来将其打印到stderr。显然,您可以自由地编写消息,只要它能产生符合语法的FORTRAN代码即可。

要使其编译通过,您需要向编译器传递标志,而这些标志在不同的编译器上略有不同。例如,以下方法适用于我:

gfortran -cpp -o errorTest errorTest.f90

也就是说,对于gfortran来说,-cpp 在编译之前调用了C预处理器。 上述程序的输出如下:

There was an error at    5 in file 
errorTest.f90
Error message: not really an error...
There was an error at   13 in file 
errorTest.f90
Error message: not an error too!

这可能会产生您所期望的效果,特别是如果您每个文件只编写一个子程序。


使用gfortran和其他一些编译器,您还可以通过使用文件类型"F90"(大写F)来调用C预处理器。 - M. S. B.
谢谢!这是一个有趣的想法。不幸的是,对于我的小型、自包含项目,我更喜欢有一个单一的文件来存放代码。但你所建议的工具对于未来的探索是非常方便的。 - drlemon

12

不,你不能。你想要实现的是称为反射的功能,在Fortran中(以及C或C++)中都不可用。


4
我发现一种简单的半自动化方法可以解决这个问题:使用正则表达式在SUBROUTINE声明后添加一个硬编码的__FUNCTION__定义。通过makefile实现,每次编译时都会刷新__FUNCTION__宏。
假设我们有一个像这样的F77清单:
文件'my-file.F'
  SUBROUTINE my_sub(var1, var2, var3)
  INCLUDE 'some-include.PRM'
  INTEGER var1
  INTEGER var2

  ! the rest of my code here
  WRITE(*,*)__FUNCTION__

  END SUBROUTINE

我想将它转换为文件'my_file.F.F'。
  SUBROUTINE my_sub(var1, var2, var3)
#undef __FUNCTION__
#define __FUNCTION__ "my_sub"
  INCLUDE 'some-include.PRM'
  INTEGER var1
  INTEGER var2

  ! the rest of my code here
  WRITE(*,*)__FUNCTION__

END SUBROUTINE

请注意,修改后的代码现在位于另一个源文件中:my-file.F.F 为了做到这一点,我向“Makefile”添加了以下行:
my-file.o: my-file.F
    perl -pne 's/^(\s+SUBROUTINE\s*)([^(]+)(\(.*\))/$$1$$2$$3\n#undef __FUNCTION__\n#define __FUNCTION__ _S($$2)/ixms' $< > $<.F; \
    $(FC) $(CPPFLAGS) $(FCFLAGS) -c $<.F -o $@

假设 FC 被定义为 Fortran 编译器可执行文件,在该文件的所有子例程上,应执行以下过程:
  • 取消定义可能已经定义的 __FUNCTION__ 宏。
  • 在 SUBROUTINE 定义下方两行添加一个 __FUNCTION__ 指令,包含子例程的名称。
  • 将文件另存为其他名称。
  • 将新源代码编译为所需的目标文件。
在这种情况下,结果应该是 my-file.o
您可能已经注意到,我还使用了 _S() 宏。这是一个“字符串化”宏。您只需要将其添加到 Fortran 文件的顶部(我将其放在一个 config.h 中,然后在所有地方都包含)。
GNU 和 Intel 有不同的实现:
#ifdef __INTEL_COMPILER
#define _S(x) #x
#else
#define _S(x) "x"
#endif 

3

有时编译器会有非标准功能,帮助你打印当前所在位置。这些高度特定于编译器,并且只应用于调试。

在gfortran中,您可以使用子程序BACKTRACE。来自手册:

BACKTRACE显示用户代码中任意位置的回溯。程序继续正常执行。

输出看起来像错误消息,但可能有所帮助。


2
为什么不在WRITE语句中直接硬编码子程序的名称?
您无法以编程方式(动态地)给出或更改子程序的名称,因此我认为没有必要尝试通过这种方式访问它(关于这一点:虽然我不确定是否可以以某种方式访问它,但我非常确定这是错误的方法...你会比硬编码更麻烦)。
顺便问一下,为什么您要尝试打印它呢?一个措辞得当的诊断消息不是更具信息性吗?

你可能是对的,硬编码子程序的名称是最简单的方法。我只是在寻找一种节省代码编辑的方法。搜索“end subroutine”并将其替换为“call digagnostic_message(); end subroutine”会很好。但是,如果子程序的名称需要传递给diagnostic_message(),那么我要么必须使用regexp搜索/替换,要么就要输入名称。在代码的50个位置进行此操作似乎是一个烦人的任务,特别是如果有自动化的方法。感谢您的回复! - drlemon
2
@drlemon - 哎呀,不要误解我的意思,因为我并不是这个意思。但你实际上正在遵循一个“他有一个问题。然后他想,“嘿,我会使用正则表达式”。现在他有两个问题。”... 你说你有小而自包含的代码。首先,在这种情况下-考虑“自动化”这些事情比手动完成更浪费时间。其次,不要想得太复杂。在需要的地方放置大量的诊断消息。如果需要,将它们与IF表达式一起放置,以便您可以轻松关闭它们。就是这样。我认识很多人,他们这样做。 - Rook
原则已经运行了几十年(成功地)。比许多现代方法要好得多。 - Rook
当然,我对此没有任何问题。我更喜欢使用打印语句进行调试,而不是使用调试器,并通过查询系统时钟来收集时间统计信息,而不是运行分析器。我认为这非常高效和简单。通过这个问题,我想找出是否有比我手动硬编码函数名称的原始方法更优雅的方式。 - drlemon

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