Search code examples
python-3.xctypes

Access Violation when using Ctypes to Interface with Fortran DLL


I have a set of dlls created from Fortran that I am running from python. I've successfully created a wrapper class and have been running the dlls fine for weeks.

Today I noticed an error in my input and changed it, but to my surprise this caused the following:

OSError: exception: access violation reading 0x705206C8

If seems that certain input values somehow cause me to try to access illegal data. I created the following MCVE and it does repeat the issue. Specifically an error is thrown when 338 < R_o < 361. Unfortunately I cannot publish the raw Fortran code, nor create an MCVE which replicates the problem and is sufficiently abstracted such that I could share it. All of the variables are either declared as integer or real(8) types in the Fortran code.

import ctypes
import os
DLL_PATH = "C:\Repos\CASS_PFM\dlls"
class wrapper:
    def __init__(self,data):
        self.data = data
        self.DLL = ctypes.CDLL(os.path.join(DLL_PATH,"MyDLL.dll"))
        self.fortran_subroutine = getattr(self.DLL,"MyFunction_".lower())
        self.output = {}

    def run(self):
        out = (ctypes.c_longdouble * len(self.data))()
        in_data = []
        for item in self.data:
            item.convert_to_ctypes()
            in_data.append(ctypes.byref(item.c_val))
        self.fortran_subroutine(*in_data, out)
        for item in self.data:
            self.output[item.name] = item.convert_to_python()

class FortranData:
    def __init__(self,name,py_val,ctype,some_param=True):
        self.name = name
        self.py_val = py_val
        self.ctype = ctype
        self.some_param = some_param

    def convert_to_ctypes(self):
        ctype_converter = getattr(ctypes,self.ctype)
        self.c_val = ctype_converter(self.py_val)
        return self.c_val

    def convert_to_python(self):
        self.py_val = self.c_val.value
        return self.py_val


def main():
    R_o = 350
    data = [
            FortranData("R_o",R_o,'c_double',False),
            FortranData("thick",57.15,'c_double',False),
            FortranData("axial_c",100,'c_double',False),
            FortranData("sigy",235.81,'c_double',False),
            FortranData("sigu",619.17,'c_double',False),
            FortranData("RO_alpha",1.49707,'c_double',False),
            FortranData("RO_sigo",235.81,'c_double',False),
            FortranData("RO_epso",0.001336,'c_double',False),
            FortranData("RO_n",6.6,'c_double',False),
            FortranData("Resist_Jic",116,'c_double',False),
            FortranData("Resist_C",104.02,'c_double',False),
            FortranData("Resist_m",0.28,'c_double',False),
            FortranData("pressure",15.51375,'c_double',False),
            FortranData("i_write",0,'c_int',False),
            FortranData("if_flag_twc",0,'c_int',),
            FortranData("i_twc_ll",0,'c_int',),
            FortranData("i_twc_epfm",0,'c_int',),
            FortranData("i_err_code",0,'c_int',),
            FortranData("Axial_TWC_ratio",0,'c_double',),
            FortranData("Axial_TWC_fail",0,'c_int',),
            FortranData("c_max_ll",0,'c_double',),
            FortranData("c_max_epfm",0,'c_double',)
    ]
    obj = wrapper(data)
    obj.run()
    print(obj.output)


if __name__ == "__main__": main()

It's not just the R_o value either; there are some combinations of values that cause the same error (seemingly without rhyme or reason). Is there anything within the above Python that might lead to an access violation depending on the values passed to the DLL?

Python version is 3.7.2, 32-bit


Solution

  • I see 2 problems with the code (and a potential 3rd one):

    1. argtypes (and restype) not being specified. Check [SO]: C function called from Python via ctypes returns incorrect value (@CristiFati's answer) for more details

    2. This may be a consequence (or at least it's closely related to) the previous. I can only guess without the Fortran (or better: C) function prototype, but anyway there is certainly something wrong. I assume that for the input data things should be same as for the output data, so the function would take 2 arrays (same size), and the input one's elements would be void *s (since their type is not consistent). Then, you'd need something like (although I can't imagine how would Fortran know which element contains an int and which a double):

      in_data (ctypes.c_void_p * len(self.data))()
      for idx, item in enumerate(self.data):
          item.convert_to_ctypes()
          in_data[index] = ctypes.addressof(item.c_val)
      
    3. Since you're on 032bit, you should also take calling convention into account (ctypes.CDLL vs ctypes.WinDLL)

    But again, without the function prototype, everything is just a speculation.

    Also, why "MyFunction_".lower() instead of "myfunction_"?