pleiszenburg / zugbruecke

Calling routines in Windows DLLs from Python scripts running under Linux, MacOS or BSD
https://zugbruecke.readthedocs.io/en/latest/
GNU Lesser General Public License v2.1
113 stars 11 forks source link

Using DISCON.DLL on linux systems #68

Closed stefnet00 closed 4 years ago

stefnet00 commented 4 years ago

I'm working on a program to simulate wind turbine dynamics. Therefore I'd like to include blade pitch controllers of various turbines which are mostly shared by .dll files. I was excited when I found zugbruecke but unfortunately I could not get it running yet. Do you have any ideas what's wrong?

    import zugbruecke as ctypes

    DLLPATH = "./5MW_Baseline-ServoData/DISCON_Win32.dll"
    DISCONDLL = ctypes.windll.LoadLibrary(DLLPATH)
    DISCON = DISCONDLL.DISCON

    INFILE = b"./DISCON.IN"

    avrSWAPVal = [0.0]*85
    avrSWAPVal[49] = 32
    avrSWAPVal[50] = len(INFILE)
    avrSWAPVal[51] = 32
    aviFAIL = ctypes.c_int(0)
    accINFILE = ctypes.create_string_buffer(int(avrSWAPVal[50]))
    accINFILE.value = INFILE
    avcOUTNAME = ctypes.create_string_buffer(int(avrSWAPVal[51]))
    avcMSG = ctypes.create_string_buffer(int(avrSWAPVal[49]))

    DISCON.argtypes = (ctypes.POINTER(ctypes.c_float), type(aviFAIL), type(accINFILE), type(avcOUTNAME), type(avcMSG))
    DISCON.memsync = [
            {
                    "p": [0],
                    "n": True,
                    "t": "c_float"
                    }
            ]

    ctypes_float_values = ((ctypes.c_float)*len(avrSWAPVal))(*avrSWAPVal)
    ctypes_float_pointer_firstelement = ctypes.cast(
            ctypes.pointer(ctypes_float_values), ctypes.POINTER(ctypes.c_float)
            )
    DISCON(ctypes_float_pointer_firstelement, aviFAIL, accINFILE, avcOUTNAME, avcMSG)
    Result[:] = ctypes_float_values[:]

DISCON is written in fortran and begins with the following lines:

    SUBROUTINE DISCON ( avrSWAP, aviFAIL, accINFILE, avcOUTNAME, avcMSG ) BIND (C, NAME='DISCON')

    USE, INTRINSIC :: ISO_C_Binding

    IMPLICIT                        NONE

    REAL(C_FLOAT),          INTENT(INOUT) :: avrSWAP   (*)                  ! The swap array, used to pass data to, and receive data from, the DLL controller. 
    INTEGER(C_INT),         INTENT(INOUT) :: aviFAIL                        ! A flag used to indicate the success of this DLL call set as follows: 0 if the DLL call was successful, >0 if the DLL call was successful but cMessage should be issued as a warning messsage, <0 if the DLL call was unsuccessful or for any other reason the simulation is to be stopped at this point with cMessage as the error message.
    CHARACTER(KIND=C_CHAR), INTENT(IN)    :: accINFILE (NINT(avrSWAP(50)))  ! The name of the parameter input file, 'DISCON.IN'.
    CHARACTER(KIND=C_CHAR), INTENT(IN)    :: avcOUTNAME(NINT(avrSWAP(51)))  ! OUTNAME (Simulation RootName) 
    CHARACTER(KIND=C_CHAR), INTENT(INOUT) :: avcMSG    (NINT(avrSWAP(49)))  ! MESSAGE (Message from DLL to simulation code [ErrMsg])  The message which will be displayed by the calling program if aviFAIL <> 0.        

avrSWAP has a fixed length and the return variable (DISCON) is not set.

The files DISCON_Win32.dll, its entire source file, and DISCON.IN can be found in the windows archive of FAST8 (I don't know if I'm allowed to upload them somewhere): https://nwtc.nrel.gov/FAST8

The arguments are not correct and the call should give an error message. But what I get by now is:

OSError: exception: access violation writing 0x00000000

s-m-e commented 4 years ago

Disclaimer: I have not (yet) tested this against the actual DLL, but thanks for providing a link to it.

Question: Can you point me to some documentation of this function OR any kind of header files / C headers / C++ headers? Or do you have a working piece of code in another programming language that actually successfully calls this function?

From what I can see, there are a few conceptual mistakes. I tried to fix and annotate your code as far as possible:

import zugbruecke as ctypes

DLLPATH = "./5MW_Baseline-ServoData/DISCON_Win32.dll"
DISCONDLL = ctypes.windll.LoadLibrary(DLLPATH)
DISCON = DISCONDLL.DISCON

INFILE = b"./DISCON.IN"

data_length = 85

avrSWAPVal = [0.0] * data_length # list of Python floats, fixed length 85
avrSWAPVal[49] = 32
avrSWAPVal[50] = len(INFILE)
avrSWAPVal[51] = 32

aviFAIL = ctypes.c_int(0) # c_int

accINFILE = ctypes.create_string_buffer(int(avrSWAPVal[50])) # null-terminated string
accINFILE.value = INFILE

avcOUTNAME = ctypes.create_string_buffer(int(avrSWAPVal[51])) # null-terminated string

avcMSG = ctypes.create_string_buffer(int(avrSWAPVal[49])) # null-terminated string

DISCON.argtypes = (
    ctypes.POINTER(ctypes.c_float * data_length), # appears to be of constant size?
    ctypes.c_int, # aviFAIL
    ctypes.POINTER(ctypes.c_char), # accINFILE
    ctypes.POINTER(ctypes.c_char), # avcOUTNAME
    ctypes.POINTER(ctypes.c_char), # avcMSG
    )
DISCON.memsync = [
    {
        'p': [2], # accINFILE
        'n': True, # null-terminated string
        },
    {
        'p': [3], # avcOUTNAME
        'n': True, # null-terminated string
        },
    {
        'p': [4], # avcMSG
        'n': True, # null-terminated string
        },
    ]

ctypes_float_values = (ctypes.c_float * data_length)(*avrSWAPVal)

DISCON(
    ctypes.pointer(ctypes_float_values), # must convert to pointer
    aviFAIL, # passed as value
    accINFILE, # is pointer, pass as is
    avcOUTNAME, # is pointer, pass as is
    avcMSG, # is pointer, pass as is
    )

Result[:] = ctypes_float_values[:]

Based on your example, I am not entirely sure in what kind of way the functions expects its first argument, i.e. avrSWAPVal. Is it an array/vector of fixed length or of dynamic/flexible length? I am just guessing from your example that it is fixed length (85) because length information is not passed to the function through any other argument. Your original code indicated that it was zero-terminated (potentially of dynamic length), which (at the moment) only works for strings (char arrays).

I have fixed memsync for the string-buffer arguments. Both argtypes and memsync should work if avrSWAPVal is of constant length. If not, I'd need to know whether or not the function receives some information on its actual dynamic length.

stefnet00 commented 4 years ago

I've just updated the question with added definition of the DISCON routine.

s-m-e commented 4 years ago

Thanks for the added information.

From context, I'd guess that aviFAIL should be passed as a pointer: A flag used to indicate the success of this DLL call set as follows: 0 if the DLL call was successful, >0 if the DLL call was successful but cMessage should be issued as a warning messsage, <0 if the DLL call was unsuccessful or for any other reason the simulation is to be stopped at this point with cMessage as the error message. I suppose the DLL function sets this value and you are supposed to read it after the function call.

With respect to avrSWAP, I still have no idea about its length. The swap array, used to pass data to, and receive data from, the DLL controller. ... does not indicate any information on constant/dynamic length or where to find length information. How did you come up with 85?

stefnet00 commented 4 years ago

I could not find any definition document of DISCON yet. As i know avrSWAP is fixed length. 85 was just the highest index accessed.

stefnet00 commented 4 years ago

Thanks a lot s-m-e, I could make some progress thanks to your help.

I could find a header file in C which declares the DISCON routine:

    #include <stdio.h>
    #include <string.h>
    extern "C"

    { void __declspec(dllexport) __cdecl DISCON(float *avrSwap, int *aviFail,
    char *accInfile, char *avcOutname, char *avcMsg);
    }

I also found a documentation which states:

So you were guessing right, aviFAIL should be passed as a pointer. I have also checked this by changing its initial value. If set to 0, the error message is:

access violation writing 0x00000000

If it is set to 100, the error message is:

access violation writing 0x00000064

My current script is

    import zugbruecke as ctypes

    DLLPATH = "./5MW_Baseline-ServoData/DISCON_Win32.dll"
    DISCONDLL = ctypes.windll.LoadLibrary(DLLPATH)
    DISCON = DISCONDLL.DISCON

    INFILE = b"./DISCON.IN"

    data_length = 85

    avrSWAPVal = [0.0] * data_length # list of Python floats, fixed length 85
    avrSWAPVal[49] = 32
    avrSWAPVal[50] = len(INFILE)
    avrSWAPVal[51] = 32

    aviFAIL = ctypes.c_int(0) # c_int

    accINFILE = ctypes.create_string_buffer(int(avrSWAPVal[50])) # null-terminated string
    accINFILE.value = INFILE

    avcOUTNAME = ctypes.create_string_buffer(int(avrSWAPVal[51])) # null-terminated string

    avcMSG = ctypes.create_string_buffer(int(avrSWAPVal[49])) # null-terminated string

    DISCON.argtypes = (
        ctypes.POINTER(ctypes.c_float * data_length), # appears to be of constant size?
        ctypes.POINTER(ctypes.c_int),  # aviFAIL
        ctypes.POINTER(ctypes.c_char), # accINFILE
        ctypes.POINTER(ctypes.c_char), # avcOUTNAME
        ctypes.POINTER(ctypes.c_char), # avcMSG
        )
    DISCON.memsync = [
        {
            'p': [2], # accINFILE
            'n': True, # null-terminated string
            },
        {
            'p': [3], # avcOUTNAME
            'n': True, # null-terminated string
            },
        {
            'p': [4], # avcMSG
            'n': True, # null-terminated string
            },
        ]

    ctypes_float_values = (ctypes.c_float * data_length)(*avrSWAPVal)

    DISCON(
        ctypes.pointer(ctypes_float_values), # must convert to pointer
        aviFAIL, # passed as value (referring to test_devide.py)
        accINFILE, # is pointer, pass as is
        avcOUTNAME, # is pointer, pass as is
        avcMSG, # is pointer, pass as is
        )

    Result[:] = ctypes_float_values[:]

The result is still an unwanted error message and I have no clue what's wrong:

ValueError: Procedure probably called with too many arguments (20 bytes in excess)
s-m-e commented 4 years ago

20 bytes in excess and the __cdecl in your C header are telling you that you tried to use the wrong calling convention. Use ctypes.cdll.LoadLibrary(DLLPATH) instead of ctypes.windll.LoadLibrary(DLLPATH).

Besides, this may not account for all 20 bytes (I am not sure). Your OS is Linux or OS X, likely 64 Bit. zugbruecke defaults to 32 bit Wine. If your DLL is 32 bit (very likely or you should have run into other issues), this is fine, bit it implies integers of different default lengths (64 bit Unix vs 32 bit "Windows"). Try specific lengths for aviFAIL as in ctypes.c_int16 or ctypes.c_int32 instead of ctypes.c_int if you are still running into issues with the number of arguments.

float *avrSwap (as found in your C header) is a pointer to a float or float array without length information. It can have arbitrary length. I guess you have to figure out where and how the memory for this pointer is being allocated. Judging by your documentation, it is not your function which is responsible for allocating this memory. Or maybe it is? Maybe there are other functions that you have to call in advance for setting the stage (and allocating memory for this pointer).

You can get better test results by eliminating zugbruecke's communication layer from the equation. zugbruecke offers a command named wine-python. It allows to run a Windows-version of Python directly on Wine. There you can use the actual ctypes instead of zugbruecke for debugging a little further. I fear though that either way you have to understand where float *avrSwap is coming from.

stefnet00 commented 4 years ago

Well, I'm way to little into programming in general to understand these hints of __cdecl and 20 bytes in excess.

Thanks to your great support DISCON is executed without any system error now. It finishes with an error but that's due to the input values, not types. I have to fill avrSWAP with correct values.

avrSWAP has a default length of 164 and is then extended depending on some of its entries. Lets say the length is 164 + avrSWAP[10] and avrSWAP[10]=10 the resulting length is 174.

There is just another short question about the input file given in accINFILE? Where do I have to copy the DISCON.IN or what could be a correct path given in accINFILE to make the input file available for the DLL?

s-m-e commented 4 years ago

Well, I'm way to little into programming in general to understand these hints of __cdecl and 20 bytes in excess.

That's more than ok. It took me ages to understand them ;)

There should really be some documentation in avrSWAP somewhere. Otherwise, I'd implemented it as fixed-length for the time being but make it "much" longer, i.e. 1000 elements or something similarly "ridiculous". This should help with debugging.

There is just another short question about the input file given in accINFILE? Where do I have to copy the DISCON.IN or what could be a correct path given in accINFILE to make the input file available for the DLL?

I was wondering about this, too. It all depends on the DLL. Just to be on the safe side, I'd copy all stuff into your current working directory, i.e. where your Python script resides and where you run it. Once this is working, you can play with relative and absolute paths etc. Beyond that, the corresponding documentation is bizarre. For instance:

accInfile is a char-array (a string in modern terms), which is totally fine. The DLL needs to know about its length because its developer did not use null-termination - also a common pattern. So passing the length through a separate parameter (avrSWAP[50]) is fine - but it really should be an integer. In your case, the length is passes within an array of single-precision floating point numbers. This has the potential to blow up in a few facinating ways. It's not your fault - it's the fault of the DLL's developer. Anyway. I guess the following might work:

import zugbruecke as ctypes

DLLPATH = "./5MW_Baseline-ServoData/DISCON_Win32.dll"
DISCONDLL = ctypes.cdll.LoadLibrary(DLLPATH)
DISCON = DISCONDLL.DISCON

INFILE = b'DISCON.IN' # relative path without prefix pointing to CWD: './'
OUTNAME = b'DISCON.OUT' # relative path without prefix pointing to CWD: './'

data_length = 1024
string_buffer_length = 1024

avrSWAPVal = [0.0 for pos in range(data_length)] # list of Python floats, fixed length data_length

aviFAIL = ctypes.c_int32(0) # c_int - correct size? On Windows, this should be 32 bit

accINFILE = ctypes.create_string_buffer(string_buffer_length) # null-terminated string
accINFILE.value = INFILE
avrSWAPVal[50] = float(len(INFILE)) # bizarre

avcOUTNAME = ctypes.create_string_buffer(string_buffer_length) # null-terminated string
avcOUTNAME.value = OUTNAME
avrSWAPVal[51] = float(len(OUTNAME)) # bizarre

avcMSG = ctypes.create_string_buffer(string_buffer_length) # null-terminated string
avcMSG.value = b' ' * string_buffer_length
avrSWAPVal[49] = float(string_buffer_length) # bizarre

DISCON.argtypes = (
    ctypes.POINTER(ctypes.c_float * data_length), # appears to be of constant size?
    ctypes.POINTER(ctypes.c_int32),  # aviFAIL
    ctypes.POINTER(ctypes.c_char), # accINFILE
    ctypes.POINTER(ctypes.c_char), # avcOUTNAME
    ctypes.POINTER(ctypes.c_char), # avcMSG
    )
DISCON.memsync = [
    {
        'p': [2], # accINFILE
        'n': True, # null-terminated string
        },
    {
        'p': [3], # avcOUTNAME
        'n': True, # null-terminated string
        },
    {
        'p': [4], # avcMSG
        'n': True, # null-terminated string
        },
    ]

ctypes_float_values = (ctypes.c_float * data_length)(*avrSWAPVal)

DISCON(
    ctypes.pointer(ctypes_float_values), # must convert to pointer
    aviFAIL, # passed as value (referring to test_devide.py)
    accINFILE, # is pointer, pass as is
    avcOUTNAME, # is pointer, pass as is
    avcMSG, # is pointer, pass as is
    )

Result[:] = ctypes_float_values[:] 
s-m-e commented 4 years ago

I probably made a mistake in declaring avcMSG correctly for your DLL. I edited the script in my last reply accordingly.

stefnet00 commented 4 years ago

The script now does what I expect. I can call the DLL and receive useful outputs. Thats great! Thanks a lot!

There was only one thing with the indices. They start with 1 in the Fortran code and the documentation but with 0 in python. So I added a dummy entry at the beginning of avrSWAPVal which is not transferred to ctypes_float_values in order to have the same index numbers:

ctypes_float_values = (self.ctypes.c_float * len(avrSWAPVal[1:]))(*avrSWAPVal[1:])

self.DISCON(
    self.ctypes.pointer(ctypes_float_values), # must convert to pointer
    self.aviFAIL, # passed as value (referring to test_devide.py)
    self.accINFILE, # is pointer, pass as is
    self.avcOUTNAME, # is pointer, pass as is
    self.avcMSG, # is pointer, pass as is
    )

self.avrSWAPVal[1:] = ctypes_float_values[:]
s-m-e commented 4 years ago

That's great to hear. Good luck with your work :)