Intel® Fortran Compiler
Build applications that can scale for the future with optimized code designed for Intel® Xeon® and compatible processors.
28446 Discussions

Error Trapping between C# and Fortran DLL

Ibrahim_A_
Novice
5,225 Views

I have a Fortran DLL code that includes a lot of modules. The DLL is called by a C# code. I am trying to make the program exit gracefully without crashing if an error is encountered on the Fortran side. Also, I'd like to send error messages back to the C# side.

I am aware from some research that error trapping has been an issue between C# and Fortran. But, is there any update on that? Can the most recent compilers trap errors in Fortran between C# and Fortran?

0 Kudos
41 Replies
Ibrahim_A_
Novice
1,943 Views

@Steve_Lionel 

I see you put stop at the end of the handler function. Even for this kind of exception, the program cannot return to a caller caller if I put a return statement instead?

0 Kudos
Steve_Lionel
Honored Contributor III
1,940 Views

Some errors are continuable, some are not. This one is not. (There is an argument to the handler that tells you which it is.) Just what would you want to return to for an uninitialized variable? Read the documentation for ESTABLISHQQ for details on how it is used.

It sounds to me as if you are relying on exception handling as a substitute for defensive programming. Uninitialized variables are simply programming errors, and you should not be looking to "recover" from them.

0 Kudos
Ibrahim_A_
Novice
1,928 Views

@Steve_Lionel 

I used the uninitialized error as an example. I have a simple question. Can the traceback information or the error message printed? Or can it be reported somewhere so I can access it? 

0 Kudos
andrew_4619
Honored Contributor II
1,923 Views

Fortran runtime Traceback can be directed to a file with an environment setting.

setenvqq("FOR_DIAGNOSTIC_LOG_FILE=C:\tracebackqq.txt")

0 Kudos
Ibrahim_A_
Novice
1,894 Views

@Steve_Lionel 

I tried your sample code that you sent inside a dll project. The handler function can capture division by zero and uninitialized variables but it cannot capture the square root of a negative number even though /fpe:0 is set. This happened only in the DLL project but when I use the console project, it gets captured by the handler as it should. Here are the codes I'm using. Is there something special that needs to be done with DLL projects?

The DLL code is

subroutine check(n1)
    
    !DEC$ ATTRIBUTES DLLEXPORT, STDCALL :: check
    !DEC$ ATTRIBUTES ALIAS:'check' :: check
    !DEC$ ATTRIBUTES REFERENCE :: n1
    
use ifestablish
implicit none
real(8) n
real(8),intent(in) :: n1
procedure(establishqq_handler) :: handler
integer(int_ptr_kind()) :: context
logical :: ret

ret = establishqq(handler, context)
n=(n1)**0.5
print *, n
end subroutine check

function handler (error_code, continuable, message_string, context)
implicit none
logical :: handler
integer, intent(in) :: error_code
logical, intent(in) :: continuable
character(*), intent(in) :: message_string
integer(int_ptr_kind()), intent(in) :: context
character(200) diag

print *, "Exiting due to ", message_string
open(1,file='traceback.txt')
write(1,*)message_string
close(1)
read(*,*)
stop
handler = .true.
end function handler

 

And here is the caller

program call_check
    !DEC$ ATTRIBUTES DLLIMPORT, ALIAS: 'check' :: check
    implicit none
    real(8)::n1
    n1=-8.0
    call check(n1)
end program call_check

 

0 Kudos
Steve_Lionel
Honored Contributor III
1,887 Views

Your DLL routine source declares the routine as STDCALL and gives the REFERENCE attribute to the argument N, but the caller doesn't match this. For this example, where it never returns to the caller, that's not fatal, but it would create problems in a real application.

As a fix for this, I just removed the STDCALL and the test code worked in that the handler was invoked for a floating invalid. 

If you want to do this in a DLL, where you can't depend on the main program being compiled with /fpe:0, add the option /fpe-all:0 to the DLL under Command Line > Additional Options. There is not a separate property for this option.

0 Kudos
Ibrahim_A_
Novice
1,878 Views

@Steve_Lionel 

It worked on my end also. Is it possible to put the Handler function in a module so that it can be used by different modules?

I tried to do that but I get this error

"error #6401: The attributes of this name conflict with those made accessible by a USE statement. [HANDLER]"

When I use the module that has the function in the DLL subroutine.

0 Kudos
Steve_Lionel
Honored Contributor III
1,870 Views

Yes, the handler can be in a module. That error happens when you have a local name that is the same as one that comes from a module.

Be aware that when you establish a handler, that "sticks" until you revert it. You may want to call ESTABLISHQQ again at the end of the routine to remove the handler before returning.

0 Kudos
Ibrahim_A_
Novice
1,865 Views

@Steve_Lionel 

I'm still struggling with how to declare the handler as a procedure in the main routine, use it in the establishqq call and also include the handler function that has the same name "handler" in a separate module. I'd definitely get an error and the names have to be the same.

I have looked in the documentation on how to revert the handler but I couldn't find help.

Can you please elaborate more?

0 Kudos
Steve_Lionel
Honored Contributor III
1,855 Views

You don't declare the handler separately if it's in a module you use. As long as the handler itself has the correct interface, you can just leave out the "procedure(establishqq_handler)" line. I put that in the example because I wasn't using a module.

To revert, save the "prev_handler" from the first ESTABLISHQQ call as a procedure pointer, and then pass that as the handler to ESTABLISHQQ at the end of the routine.

0 Kudos
Ibrahim_A_
Novice
1,833 Views

Okay. It's working now. I have a few questions to understand how the handler function and the establishqq work.

In the handler function, why did you set the value of the handler to "True" after "Stop" statement?

When you say reverting the handler because the value "sticks", what does that mean?

I have tried to follow your instruction blindly and this is what I ended up doing. Is this correct?

subroutine check(n1)
    
    !DEC$ ATTRIBUTES DLLEXPORT :: check
    !DEC$ ATTRIBUTES ALIAS:'check' :: check

use interop_c    ! the module that has the handler function
use ifestablish
implicit none
real(8) n
real(8),intent(in) :: n1
procedure(establishqq_handler),pointer :: old_handler
integer(int_ptr_kind()) :: old_context,context
logical :: ret

ret = establishqq(handler, context,old_handler,old_context)
n=n1/0
print *, n
ret = establishqq(old_handler, old_context)
end subroutine check
0 Kudos
Steve_Lionel
Honored Contributor III
1,830 Views

By "sticks" I mean that calling ESTABLISHQQ establishes a handler that is global to the currently running program - its effect is not local to the routine where you called ESTABLISHQQ. (In this regard it is different from the OpenVMS LIB$ESTABLISH that I based this on, but VMS has procedure-local, language-independent exception handling baked in, where Windows and Linux do not.)

I set the function result as not doing so will get you a compiler warning - it has no other effect.

Yes, it looks as if you did the revert correctly.

0 Kudos
Ibrahim_A_
Novice
1,736 Views

@Steve_Lionel 

I have /fpe-all:0 turned on. The attached fortran works fine with a fortran executable caller. However, the error gets handle by C# when I use a C# caller. I get an error message from C# that there's an arithmetic error. The establishqq command doesn't get to be executed. If I remove /fpe-all:0, nothing happens at all using the fortran and the C# caller. This issue only happens with arithmetic errors (division by zero and sqrt(-ve)). A similar fortran code worked fine with process exceptions. Any ideas why is this happening?

The Fortran Code:

subroutine check_3a(n1)
    
    !DEC$ ATTRIBUTES DLLEXPORT,STDCALL :: check_3a
    !DEC$ ATTRIBUTES ALIAS:'check_3a' :: check_3a
    !DEC$ ATTRIBUTES REFERENCE :: n1
    
use interop_c    ! the module that has the handler function
use ifestablish
implicit none
real(8) n
real(8), allocatable :: v(:)
character(1) text
real(8),intent(in) :: n1
procedure(establishqq_handler),pointer :: old_handler
integer(int_ptr_kind()) :: old_context,context
logical :: ret
character(13) diag

    diag='traceback.txt'
    call setenvqq(diag)

print *,'establish handler'
ret = establishqq(handler, context,old_handler,old_context)
print *,'start division'
n=1/n1
print *,'division attempted'
print *, n
return
!read(*,*)
ret = establishqq(old_handler, old_context)
    end subroutine check_3a
    
subroutine check_3b(n1)

The handler module is here. 

module interop_c
    
    
    
implicit none 
PROCEDURE(CSharpWriteToConsoleInterface), POINTER :: CSharpWriteToConsole
 ABSTRACT INTERFACE
 SUBROUTINE CSharpWriteToConsoleInterface(i)
 INTEGER, INTENT(INOUT) :: i
 END SUBROUTINE CSharpWriteToConsoleInterface
 END INTERFACE
 CONTAINS
!DEC$ ATTRIBUTES DLLEXPORT::SetupFunction
!DEC$ ATTRIBUTES ALIAS:'SetupFunction'::SetupFunction
SUBROUTINE SetupFunction(fPtr)
 PROCEDURE(CSharpWriteToConsoleInterface) :: fPtr
 INTEGER :: i
 ! Set Pointer
 CSharpWriteToConsole => fPtr
END SUBROUTINE SetupFunction

function handler (error_code, continuable, message_string, context)
!use interop_c
implicit none
logical :: handler
integer, intent(in) :: error_code
logical, intent(in) :: continuable
character(*), intent(in) :: message_string
integer(int_ptr_kind()), intent(in) :: context
character(200) diag
integer :: j
j=error_code
print *, "Handler entered"
print *, "Exiting due to ", message_string
open(1,file='traceback.txt')
write(1,*)message_string
close(1)
CALL CSharpWriteToConsole(j)
!read(*,*)
stop
handler = .true.
end function handler

end module interop_c

 

CALL CSharpWriteToConsole(j) sends the error code to the C# code if the dll is called by a C# caller.

Thanks!

0 Kudos
Steve_Lionel
Honored Contributor III
1,732 Views

Sorry, I have no more I can add here. I don't have a full understanding of how exception handling works on Windows (or Linux), and cross-language exceptions are especially tricky. ESTABLISHQQ can handle exceptions that get seen by the Fortran run-time, but it may be in your case the Fortran run-time never gets a chance to see them.

0 Kudos
jimdempseyatthecove
Honored Contributor III
1,709 Views

Attempting (complete) exception handling returns from a Fortran DLL will be incomplete and unsatisfactory at best.

In the C#/C++ compiler designs (that support SEH), exceptions are passed up the call stack where each returning stack level can optionally catch the exception, and more (as) importantly, when not catching and exception, perform the necessary dtor's on function scope allocated objects. Think about this....

Fortran subroutines and functions permit local allocatables, and with recent language standards, include an implicit deallocate of allocated local allocatables. Your contrived SEH method will bypass the implicit deallocations, and thus present a memory leak (should there be any "dangling" allocations).

IOW, your implementation can be successful provided that your code performs no allocations. Allocations could potentially be performed indirectly via a self-written private heap which can be re-initialized upon exit and/or exception from the DLL... but then this precludes having persistent allocated objects within the DLL.

Jim Dempsey

0 Kudos
Ibrahim_A_
Novice
1,647 Views

@jimdempseyatthecove 

I didn't have any allocation taking place in the code I shared. I wonder why I still can't get the Fortran to catch the error!

Is the procedure pointer contributing to that?

Thanks!

0 Kudos
Ibrahim_K_
New Contributor I
1,458 Views

I may have missed but could you share your C# code?

Please keep in mind that most objects and properties are kept in (managed) heap in managed C# (.NET). Even most of the numbers are stored as objects in .NET. You have, in principle, three options.

(1)  you can wrap your FORTRAN code in managed code;

(2) you unbox/box all data you share with Fortran before transferring to FORTRAN or back.

(3) allocate all data shared with Fortran in an unmanaged section of C# code.

I. Konuk

0 Kudos
FortranFan
Honored Contributor II
1,449 Views

 

@Ibrahim_A_ wrote:
.. Can the most recent compilers trap errors in Fortran between C# and Fortran?

 

If safe and sustainable ways to do exception handling are of interest, take note there is NO way to do this reliably and consistently and which may also be portable and maintainable, none whatsoever.  Now, if you look hard enough online, you may find some hacks here and there but I wouldn't use them for myself so won't recommend them to anyone.

Other than the issue with throwing exceptions with references to uninitialized variables - which is a programming error anyway with what's currently a static compiler-based, trying-to-be-type-safe language such as Fortran and you're off better off handling using more than one FOrtran compiler to test your code, using static analyzers, etc. - Fortran errors are best "caught" by the instructions in yuor code itself e.g., IOSTAT with IO such as read ( unit=lun, .., iostat=.. ); other STAT options with intrinsic subprograms; and IEEE facilities for floating-point exceptions.

Your best bet then is to keep it simple, return error codes and error messages to C# "managed" side either via your APIs and/or some callback mechanisms and do the exception handling on the C# side.   Or, move all your code to C#. 

0 Kudos
Ibrahim_K_
New Contributor I
1,684 Views

Did you try to call Fortran from an "unmanaged" section of C# code? See example here:

Mixing Managed and Unmanaged code

0 Kudos
FortranFan
Honored Contributor II
1,970 Views

 

Ibrahim_A_ wrote:
..
I was able to compile the C++/Fortran codes successfully for matrix multiplication and I added try-catch blocks in the C++ code. I'm still unable to figure out how to send the exception messages to C++ from fortran. For example, how can I pass an exception that there's a number being divided by zero or uninitiated variable?

The fortran code can detect them using Run-time error detection, but the fortran code stops working once these issues are detected without going back to the C++ code. ..

 

 

@Ibrahim_A_ ,

Since you're replying to the post by @Ibrahim_K_ where the "do not do anything fancy" is mentioned and which can be interpreted as keep-it-simple, an option you can consider is for the Fortran DLL to include a callback functionality to invoke a C++ function and that may be a simple way to handle this.

Such a C++ callback function can be invoked when an error exception can be "recognized" in Fortran e.g., using STAT=; IOSTAT= arguments in intrinsic procedures and with IEEE module procedures for floating-point.

This won't "catch" of course things such as uninitialized variables which you will have to check yourself. 

The C++ callback can try to "raise" an exception.  You can then construct try..catch blocks in C++ around the Fortran DLL procedure invocations.

0 Kudos
FortranFan
Honored Contributor II
1,995 Views
@Ibrahim_K wrote:

I am just about to finish a similar project. I will provide some of the lessons I learned.

Firstly, I link my FORTRAN code with native C++ to generate a static library. Then I wrap the static library in a C++/CLI code. Calls between C# and CLI are managed calls. You can pass any info between like you are within C#. I uploaded here in this forum my project architecture sometimes ago. 

1) I do not do anything fancy with memory on the FORTRAN side. If I need play with memory, I do it in C++. You can pass std::vector structures to FORTRAN safely.

..

3) I do not do any fancy I/O in FORTRAN.

Since things like division by zero do not produce exceptions (in the sense that FORTRAN generated code continues with "Nan" value), you can check them at a later point or even display it. ..

 

@Ibrahim_K_ ,

C++ objects generally and specifically the STL 'class' instances such as those of std::vector are technically not interoperable with Fortran, readers will benefit from your clarification re: "pass std::vector structures" and what you meant by "safely".

You mention a couple of times you "do not do any fancy" stuff in Fortran, what would be "fancy" according to you?

Re: floating-point exceptions, you write, "you can check them later point .." but deferring action on such exceptions is detrimental in most situations involving numerically intense and sensitive applications.  So why defer action on this count?

0 Kudos
Reply