pythonlinuxshared-librariesctypesundefined-symbol

Python ctypes failed call .so library on Linux, OSError: ./libEFWFilter.so.1.7: undefined symbol: udev_enumerate_new


There is a filter wheel SDK, named "libEFWFilter.so", provided by ZWO company. The downloaded link is https://dl.zwoastro.com/software?app=DeveloperEfwSdk&platform=windows86&region=China.

I want to use ctypes or CFFI module of python to call this dynamic library on Ubuntu20.04 or CentOS7. The following is my python program:test.py.

#!/usr/bin/python3

import ctypes
from ctypes.util import find_library
import os

print(find_library("udev"))
print(find_library("EFWFilter"))
print(os.getcwd() + "/libEFWFilter.so.1.7")

libefw = ctypes.cdll.LoadLibrary(os.getcwd() + "/libEFWFilter.so.1.7")

When I run this test.py, the output error messages are as follows.

libudev.so.1
libEFWFilter.so.1.7
/home/ls/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7
Traceback (most recent call last):
  File "test.py", line 11, in <module>
    libefw = ctypes.cdll.LoadLibrary(os.getcwd() + "/libEFWFilter.so.1.7")
  File "/usr/lib/python3.8/ctypes/__init__.py", line 451, in LoadLibrary
    return self._dlltype(name)
  File "/usr/lib/python3.8/ctypes/__init__.py", line 373, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: /home/ls/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7: undefined symbol: udev_enumerate_new

But I have installed the libudev library.

ls@ubuntu64:~/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64$ whereis libudev
libudev: /usr/lib/x86_64-linux-gnu/libudev.so /usr/include/libudev.h /usr/share/man/man3/libudev.3.gz

The demo provided by this company can be compiled correctly and the executable file runs OK. The demo contains "main.cpp"、"EFW_filter.h"、"Makefile". Just as follows.

main.cpp

#include "stdio.h"
#include "EFW_filter.h"
#ifdef _WINDOWS
#include <windows.h>
#else
#include <sys/time.h>
#include <unistd.h>
#define Sleep(a) usleep((a)*1000)
#endif
int  main()
{
    
    int EFW_count = EFWGetNum();  
    if(EFW_count <= 0)
    {
        printf("no filter wheel connected, press any key to exit\n");
        getchar();
        return -1;
    }
    else
        printf("attached filter wheel:\n");

    EFW_INFO EFWInfo;
    

    for(int i = 0; i < EFW_count; i++)
    {
        EFWGetID(i, &EFWInfo.ID);
        EFWGetProperty(EFWInfo.ID, &EFWInfo);
        printf("index %d: %s\n",i, EFWInfo.Name);
    }

    printf("\nselect one \n");

    int EFWIndex, iSelectedID;
    scanf("%d", & EFWIndex);
    EFWGetID(EFWIndex, &iSelectedID);

    if(EFWOpen(iSelectedID) != EFW_SUCCESS)
    {
        printf("open error,are you root?,press any key to exit\n");
        getchar();
        return -1;
    }

    
    if(iSelectedID < 0)
        {
    
            printf("open error,are you root?,press any key to exit\n");
        getchar();
        return -1;   
     }
     else
    {      
        
        EFW_ERROR_CODE err;
        while(1){
            err = EFWGetProperty(iSelectedID, & EFWInfo);
            if(err != EFW_ERROR_MOVING )
                break;
            Sleep(500);
        } 
            printf("%d slots: ", EFWInfo.slotNum);
        for(int i = 0; i < EFWInfo.slotNum; i++)
            printf("%d ", i + 1);
       
        int currentSlot;
        while(1)
            {

                err = EFWGetPosition(iSelectedID, &currentSlot);
                if(err != EFW_SUCCESS || currentSlot != -1 )
                        break;
                Sleep(500);
            } 
        printf("\ncurrent position: %d\n", currentSlot + 1);

        int targetSlot;
        char c;
        printf("\nPlease input target position, type \'q\' to quit:\n");
        while(1)
        {   
            c = getchar();
     
                if (EOF == c)
                continue;
            if(c == 'q')
                break;
            if(c >= '0' && c <= '9')
                targetSlot = c - '0';
            else
                continue;
            
            targetSlot--;
            if(targetSlot < 0 || targetSlot >=  EFWInfo.slotNum)
                continue;
            
            err = EFWSetPosition(iSelectedID, targetSlot);
            if(err == EFW_SUCCESS)
                printf("\nMoving...\n");
            while(1)
                {
                     err = EFWGetPosition(iSelectedID, &currentSlot);
                    if(err != EFW_SUCCESS ||currentSlot != -1 )
                            break;
                    Sleep(500);
                } 
            printf("\nPlease input target position, type \'q\' to quit:\n");

        }
        EFWClose(iSelectedID);
        printf("main function over\n");
        return 1;
    }
}

EFW_filter.h

/**************************************************
this is the ZWO filter wheel EFW SDK
any question feel free contact us:yang.zhou@zwoptical.com

here is the suggested procedure.

--> EFWGetNum
--> EFWGetID for each filter wheel

--> EFWGetProperty
--> EFWOpen
--> EFWGetPosition
--> EFWSetPosition
    ...
--> EFWClose

***************************************************/
#ifndef EFW_FILTER_H
#define EFW_FILTER_H


#ifdef _WINDOWS
#define EFW_API __declspec(dllexport)
#else
#define EFW_API 
#endif

#define EFW_ID_MAX 128

typedef struct _EFW_INFO
{
    int ID;
    char Name[64];
    int slotNum;
} EFW_INFO;


typedef enum _EFW_ERROR_CODE{
    EFW_SUCCESS = 0,
    EFW_ERROR_INVALID_INDEX,
    EFW_ERROR_INVALID_ID,
    EFW_ERROR_INVALID_VALUE,
    EFW_ERROR_REMOVED, //failed to find the filter wheel, maybe the filter wheel has been removed
    EFW_ERROR_MOVING,//filter wheel is moving
    EFW_ERROR_ERROR_STATE,//filter wheel is in error state
    EFW_ERROR_GENERAL_ERROR,//other error
    EFW_ERROR_NOT_SUPPORTED,
    EFW_ERROR_CLOSED,
    EFW_ERROR_END = -1
}EFW_ERROR_CODE;

typedef struct _EFW_ID{
    unsigned char id[8];
}EFW_ID;

typedef EFW_ID EFW_SN;

#ifdef __cplusplus
extern "C" {
#endif
/***************************************************************************
Descriptions:
this should be the first API to be called
get number of connected EFW filter wheel, call this API to refresh device list if EFW is connected
or disconnected

Return: number of connected EFW filter wheel. 1 means 1 filter wheel is connected.
***************************************************************************/
EFW_API int EFWGetNum();

/***************************************************************************
Descriptions:
get the product ID of each wheel, at first set pPIDs as 0 and get length and then malloc a buffer to load the PIDs

Paras:
int* pPIDs: pointer to array of PIDs

Return: length of the array.
***************************************************************************/
EFW_API int EFWGetProductIDs(int* pPIDs);

/***************************************************************************
Descriptions:
get ID of filter wheel

Paras:
int index: the index of filter wheel, from 0 to N - 1, N is returned by GetNum()

int* ID: pointer to ID. the ID is a unique integer, between 0 to EFW_ID_MAX - 1, after opened,
all the operation is base on this ID, the ID will not change.


Return: 
EFW_ERROR_INVALID_INDEX: index value is invalid
EFW_SUCCESS:  operation succeeds

***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetID(int index, int* ID);

/***************************************************************************
Descriptions:
open filter wheel

Paras:
int ID: the ID of filter wheel

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_GENERAL_ERROR: number of opened filter wheel reaches the maximum value.
EFW_ERROR_REMOVED: the filter wheel is removed.
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWOpen(int ID);

/***************************************************************************
Descriptions:
get property of filter wheel. SlotNum is 0 if not opened.

Paras:
int ID: the ID of filter wheel

EFW_INFO *pInfo:  pointer to structure containing the property of EFW

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_MOVING: slot number detection is in progress, generally this error will happen soon after filter wheel is connected.
EFW_SUCCESS: operation succeeds
EFW_ERROR_REMOVED: filter wheel is removed

***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetProperty(int ID, EFW_INFO *pInfo); 

/***************************************************************************
Descriptions:
get position of slot

Paras:
int ID: the ID of filter wheel

int *pPosition:  pointer to slot position, this value is between 0 to M - 1, M is slot number
this value is -1 if filter wheel is moving

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
EFW_ERROR_ERROR_STATE: filter wheel is in error state
EFW_ERROR_REMOVED: filter wheel is removed

***************************************************************************/    
EFW_API EFW_ERROR_CODE EFWGetPosition(int ID, int *pPosition);
    
/***************************************************************************
Descriptions:
set position of slot

Paras:
int ID: the ID of filter wheel

int Position:  slot position, this value is between 0 to M - 1, M is slot number

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
EFW_ERROR_INVALID_VALUE: Position value is invalid
EFW_ERROR_MOVING: filter wheel is moving, should wait until idle
EFW_ERROR_ERROR_STATE: filter wheel is in error state
EFW_ERROR_REMOVED: filter wheel is removed

***************************************************************************/
EFW_API EFW_ERROR_CODE EFWSetPosition(int ID, int Position);

/***************************************************************************
Descriptions:
set unidirection of filter wheel

Paras:
int ID: the ID of filter wheel

bool bUnidirectional: if set as true, the filter wheel will rotate along one direction

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWSetDirection(int ID, bool bUnidirectional);

/***************************************************************************
Descriptions:
get unidirection of filter wheel

Paras:
int ID: the ID of filter wheel

bool *bUnidirectional: pointer to unidirection value .

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetDirection(int ID, bool *bUnidirectional);

/***************************************************************************
Descriptions:
calibrate filter wheel

Paras:
int ID: the ID of filter wheel

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
EFW_ERROR_MOVING: filter wheel is moving, should wait until idle
EFW_ERROR_ERROR_STATE: filter wheel is in error state
EFW_ERROR_REMOVED: filter wheel is removed
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWCalibrate(int ID);

/***************************************************************************
Descriptions:
close filter wheel

Paras:
int ID: the ID of filter wheel

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWClose(int ID);

/***************************************************************************
Descriptions:
get version string, like "0, 4, 0824"
***************************************************************************/
EFW_API char* EFWGetSDKVersion();


/***************************************************************************
Descriptions:
get hardware error code of filter wheel

Paras:
int ID: the ID of filter wheel

bool *pErrCode: pointer to error code .

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetHWErrorCode(int ID, int *pErrCode);

/***************************************************************************
Descriptions:
Get firmware version of filter wheel

Paras:
int ID: the ID of filter wheel

int *major, int *minor, int *build: pointer to value.

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetFirmwareVersion(int ID, unsigned char *major, unsigned char *minor, unsigned char *build);

/***************************************************************************
Descriptions:
Get the serial number from a EFW

Paras:
int ID: the ID of focuser

EFW_SN* pSN: pointer to SN

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_ERROR_NOT_SUPPORTED: the firmware does not support serial number
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWGetSerialNumber(int ID, EFW_SN* pSN);

/***************************************************************************
Descriptions:
Set the alias to a EFW

Paras:
int ID: the ID of filter

EFW_ID alias: the struct which contains the alias

Return: 
EFW_ERROR_INVALID_ID: invalid ID value
EFW_ERROR_CLOSED: not opened
EFW_ERROR_NOT_SUPPORTED: the firmware does not support setting alias
EFW_SUCCESS: operation succeeds
***************************************************************************/
EFW_API EFW_ERROR_CODE EFWSetID(int ID, EFW_ID alias);

#ifdef __cplusplus
}
#endif

#endif

Makefile

platform = x64

CC = g++

ifeq ($(platform), mac32)
CFLAGS += -m32 -framework IOKit -framework CoreFoundation

endif

ifeq ($(platform), mac64)

CFLAGS += -framework IOKit -framework CoreFoundation
endif

ifeq ($(platform), mac)
CFLAGS += -arch i386 -arch x86_64 -framework IOKit -framework CoreFoundation

endif

ifeq ($(platform), x86)
CFLAGS += -ludev
endif


ifeq ($(platform), x64)
CFLAGS += -ludev
endif

ifeq ($(platform), armv5)
CFLAGS += -ludev
endif


ifeq ($(platform), armv6)
CFLAGS += -ludev

endif

ifeq ($(platform), armv8)
CFLAGS += -ludev

endif

all:test_console
test_console: main.cpp
    $(CC) main.cpp -o test_console $(CFLAGS) ../../lib/$(platform)/libEFWFilter.a -I../../include $(CFLAGS) -lpthread
    cp test_console bin/$(platform)/
clean:
    rm -f test_console

When I build demo, It's OK and no error.

The things that I have tried:

  1. Testing this program on CentOS7.
  2. Using highest version of python : python3.10.13.

But the above methods didn't solve this "undefined symbol" problem. I noticed that some solutions to solve this similar problem is add "extern C" to cpp function. But I am not able to do this, because I don't know the source code of libEFWFilter.so library. So what should I do to solve this problem by python? Thanks for your help very much!

updated on 2024/2/20

The follows is output information When I type "ctypes.CDLL("libudev.so").udev_enumerate_new" in Python console and type "dpkg -s libudev1:amd64" and "ldd ./libEFWFilter.so.1.7" in linux terminal.

ls@ubuntu64:~/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64$ python3
Python 3.8.10 (default, Nov 22 2023, 10:22:35) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> print(ctypes.CDLL("libudev.so").udev_enumerate_new)
<_FuncPtr object at 0x7f70be96bb80>
>>> from ctypes.util import find_library
>>> print(find_library("udev"))
libudev.so.1
>>> 
ls@ubuntu64:~/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64$ dpkg -s libudev1:amd64
Package: libudev1
Status: install ok installed
Priority: required
Section: libs
Installed-Size: 340
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Architecture: amd64
Multi-Arch: same
Source: systemd
Version: 245.4-4ubuntu3.23
Depends: libc6 (>= 2.30)
Description: libudev shared library
 This library provides access to udev device information.
Homepage: https://www.freedesktop.org/wiki/Software/systemd
Original-Maintainer: Debian systemd Maintainers <pkg-systemd-maintainers@lists.alioth.debian.org>
ls@ubuntu64:~/libEFW/EFW_linux_mac_SDK_V1.7/lib/x64$ ldd ./libEFWFilter.so.1.7 
        linux-vdso.so.1 (0x00007ffd491f3000)
        libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f6866b1d000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f68669ce000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f68669b3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f68667c1000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f6866f6e000)

From the above message, it seems no problem. But there is no "libudev.so" in the output of "ldd ./libEFWFilter.so.1.7", it may be a error? how to solve this problem? Thanks again.


Solution

  • The example works, but it's not the same scenario, as it links to the static library variant (../../lib/$(platform)/libEFWFilter.a). Also, it links to LibUDev (CFLAGS += -ludev), which is the 1st clue.
    As a side note, the Makefile is kind of poorly written and also the archives structures can be much improved.

    Back to our library:

    (py_pc064_03.08_test0_lancer) [cfati@cfati-5510-0:/mnt/e/Work/Dev/StackExchange/StackOverflow/q078021783]> . ~/sopr.sh
    ### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ###
    
    [064bit prompt]>
    [064bit prompt]> ldd EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7
       linux-vdso.so.1 (0x00007fff72300000)
       libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f5ac3600000)
       libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5ac3919000)
       libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f5ac38f9000)
       libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5ac3200000)
       /lib64/ld-linux-x86-64.so.2 (0x00007f5ac3c7f000)
    [064bit prompt]>
    [064bit prompt]> nm -S EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7 | grep udev
                     U udev_device_get_devnode
                     U udev_device_get_parent_with_subsystem_devtype
                     U udev_device_get_sysattr_value
                     U udev_device_new_from_devnum
                     U udev_device_new_from_syspath
                     U udev_device_unref
                     U udev_enumerate_add_match_subsystem
                     U udev_enumerate_get_list_entry
                     U udev_enumerate_new
                     U udev_enumerate_scan_devices
                     U udev_enumerate_unref
                     U udev_list_entry_get_name
                     U udev_list_entry_get_next
                     U udev_new
                     U udev_unref
    

    As already mentioned, the library doesn't depend on (link to) LibUDev (libudev.so*), but it uses some of its functions (udev_*).

    It's a common behavior on Nix that libraries depend on symbols that will be present in the loading executable. Now, the symbols source might be the executable itself or other libraries already loaded. This is the case for Python extension modules (e.g.: _ctypes*.so), which depend on symbols from PYTHONCORE (like Py_Initialize) that are defined inside python executable. But, if it's a shared build, the symbols will be defined in libpython*.so (that the executable links to).

    It's the same thing here. All we have to do is (prerequisites):

    1. Loading libudev.so*

    2. Make its symbols (udev_enumerate_new) visible to libEFWFilter.so* (when it will be loaded)

    Example:

    [064bit prompt]> python
    Python 3.8.18 (default, Aug 25 2023, 13:20:30) 
    [GCC 11.4.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    >>> import ctypes as cts
    >>>
    >>> lib = "./EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7"
    >>>
    >>> # --- Attempt loading our library ---
    >>> cts.CDLL(lib)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/usr/lib/python3.8/ctypes/__init__.py", line 373, in __init__
        self._handle = _dlopen(self._name, mode)
    OSError: ./EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7: undefined symbol: udev_enumerate_new
    >>>
    >>> # --- Load LibUDev into the Python process ---
    >>> cts.CDLL("libudev.so")
    <CDLL 'libudev.so', handle 28056b0 at 0x7f6cfeda3910>
    >>>
    >>> # --- Attempt loading our library ---
    >>> cts.CDLL(lib)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/usr/lib/python3.8/ctypes/__init__.py", line 373, in __init__
        self._handle = _dlopen(self._name, mode)
    OSError: ./EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7: undefined symbol: udev_enumerate_new
    >>>
    >>> # --- Load LibUDev into the Python process (actually, it's already loaded by the previous call) ---
    >>> # --- AND make its symbols visible ---
    >>> cts.CDLL("libudev.so", mode=cts.RTLD_GLOBAL)  # !!!
    <CDLL 'libudev.so', handle 28056b0 at 0x7f6cfedc5d90>
    >>>
    >>> # --- Attempt loading our library ---
    >>> cts.CDLL(lib)
    <CDLL './EFW_linux_mac_SDK_V1.7/lib/x64/libEFWFilter.so.1.7', handle 2807da0 at 0x7f6cff10e400>
    

    Might want to also check: