Python development - miscellaneous notes on Python mixed programming of Python and C/C + +

Python is a scripting language that can do many things. This article will explain how to use Python to call C/C + libraries.

For the creation of C/C + + library, please refer to the author's previous articles. Here, only the creation process under Window is given.

Static and dynamic libraries (Windows version)

Static and dynamic libraries (Linux)

1 Windows create dynamic library

1.1 generate dynamic library

1. First create a dynamic library project.

To cancel the precompiled header, select DLL here.

2. Create header file (. h) and source file (. cpp)

DynamicLib.h

#ifndef _DYNAMIC_LIB_H_
#define _DYNAMIC_LIB_H_

#include <iostream>

#define DLL_EXPORTS

#ifdef DLL_EXPORTS
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif

namespace DynamicLib
{
	DLL_API void print_hello();
}
#endif // _DYNAMIC_LIB_H_

DynamicLib.cpp

#include "DynamicLib.h"

DLL_API  void DynamicLib::print_hello()
{
	std::cout << "Hello world!" << std::endl;
}

[Note 1] _declspec(dllexport) is an export symbol, that is, the function declaration of the exported function in the DLL that defines the function to be exported is preceded by an export symbol, indicating that the method can be exported to other DLLs or exe.
[Note 2] extern "C" is added to C + + code to ensure that the function name generated during compilation remains unchanged, so that the corresponding function can be found when calling dll dynamically.

3. Select release to generate the solution. Here, select 64 bit.

The last generated library is shown in the following figure:

OK, the dynamic library is created.

1.2 use of dumpbin tool

dumpbin.exe is a Microsoft COFF binary converter that displays information about common object file format (COFF) binaries. Dumpbin can be used to check COFF object files, standard COFF object libraries, executable files, dynamic link libraries, etc.

dumpbin.exe is located in the VS installation directory \ VC \ bin \ dumpbin Exe, you can also run it through the Visual Studio developer command prompt in the start menu.

dumpbin usage:

dumpbin [option] [file name]

Multiple options are separated by spaces, and multiple file names are also separated by spaces. File names can be suffixed with obj,. lib,. dll,. exe, if no input file is specified, it will list all options.

Option Description: the parameter can be used with "-" or "/" (for example, - ALL equals / ALL) followed by the option name. Some options can be followed by ":" after the option name. Use spaces or tabs to split command options. Option names, keywords, and file names are case insensitive. Most parameters can be applied to ALL binary files, and a few parameters can only be used for specific files.

(1)/ALL: this option displays ALL available information except code disassembly. Use / DISASM to display disassembly. You can use / RAWDATA:NONE with / ALL to omit the original binary details of the file.

(2)/ARCHIVEMEMBERS: this option displays the minimum information about library member objects. (3) , / cleaner file: where file is the image file generated with / clr. Clearleader displays information about the used in any managed program net header. Output display net header and the position and size of its sections (in bytes).

(3)/DIRECTIVES: this option dumps compiler generated in the image directive section.

(4) / requirements: name of the DLL from which the dump image was imported. Do not dump import function names.

(5)/DISASM: this option displays the disassembly of code snippets and uses symbols if they appear in files.

(6)/EXPORTS: this option displays all definitions exported from an executable or DLL.

(7)/FPO: this option displays frame pointer optimization (FPO) records.

(8)/HEADERS: this option displays the file header and the header of each section. When used with libraries, displays the header of each member object.

(9)/IMPORTS[:file]: this option displays the list of DLLs imported into executable files or DLLs (statically linked and delayed loading) and the respective imports of each of the above DLLs. The optional file specification allows you to specify that only imports of a DLL are displayed.

(10)/LINENUMBERS: this option displays COFF line numbers. If the object file is compiled with a program database (/ Zi), C7 compatible (/ Z7), or line number only (/ Zd), it contains line numbers. If the executable or DLL is linked to generate debug information (/ DEBUG), it contains the COFF line number.

(11)/LINKERMEMBER[:{1|2}]: this option displays common symbols defined in the library. Specifying parameter 1 displays symbols and their offsets in object order. Specifying parameter 2 displays the offset and index number of objects, and then trains these symbols and the object index of each symbol in alphabetical order. To get both outputs, specify / LINKERMEMBER without numeric parameters.

(12) / loadconfig: this option dumps IMAGE_LOAD_CONFIG_DIRECTORY structure, which is used by the Windows NT loader and in wiinnt Optional structures defined in H.

(13)/OUT:filename: this option specifies the filename of the output. By default, DUMPBIN displays information to standard output.

(14)/PDBPATH[:VERBOSE]filename: filename is the name for which you want to find a match Of PDB files dll or exe file name. Verbose (optional) is the report in which an attempt was made to locate All directories of PDB files/ PDBPATH will search along the debugger The same path as the pdb file searches the computer and will report those The pdb file (if any) corresponds to the file specified in filename.

(15)/RAWDATA[:{1|2|4|8|NONE}[,number]]: this option displays the original content of each section in the file. Parameter Description: 1. Default value. The content is displayed in hexadecimal bytes. If the content has a printed representation, it is also displayed as ASCII characters; 2. The content is displayed as a hexadecimal 2-byte value; 4. The content is displayed as hexadecimal 4-byte value; 8. The content is displayed as a hexadecimal 8-byte value; NONE, cancel the display of original data. This parameter is very useful for controlling / ALL output; Number, the displayed rows are set to a width of number values per row.

(16)/RELOCATIONS: this option displays any relocations in the object or image.

(17)/SECTION:section: this option limits the output of information related to the specified section.

(18)/SUMMARY: this option displays the minimum information (including the total size) of the joint. If no other options are specified, this option is the default.

(19)/SYMBOLS: this option displays the COFF symbol table. Symbol tables exist in all object files. For an image file, it contains a COFF symbol table only when it is linked to / DEBUG.

(20)/UNWINDINFO: store the expansion descriptor of structured exception handling (SHE) table in program images (such as exe and dll)/ UNWINDINFO is only available for IA64 images.

The complete options can be viewed in MSDN documents.

dumpbin

This article mainly uses dumpbin to view the dynamic library functions:

#dumpbin -exports xxx.dll

As can be seen from the above figure, the dynamic library exports a function, which is consistent with the exported function in the above code.

It is worth noting that if you want to export the dynamic library, you need to add an extern "C" statement, otherwise C + + will tamper with the name of the function according to its own rules. C + + supports function overloading, which is to record the relevant parameter information of the function in the function name adaptation stage. The C + + standard does not define the standard of name adaptation, so the dynamic libraries compiled by different compilers cannot be universal.

The C standard specifies the standard for name adaptation. Extern "C" tells the compiler to compile the code according to the C standard. I believe many embedded friends have seen extern "C" statements..

In addition, it should be noted that if the function modified by extern "C" is overloaded, an error will be reported during compilation, because C language does not support function overloading.



2 Python calls C/C + + dynamic library

Python calls dynamic libraries through ctypes, a built-in package. Ctypes provides C compatible data types, allowing calls to functions in DLL s or shared libraries. Through this module, you can use Python code to call these libraries, which is very convenient.

ctypes is suitable for "medium and lightweight" Python C/C + + mixed programming. ctypes is especially convenient when a third-party library provides dynamic link libraries and call documents, and there is no compiler or the compilers are not compatible with each other. It is worth noting that for certain requirements that can be implemented by Python itself (such as obtaining system time, reading and writing files, etc.), you should give priority to using Python's own functions rather than the API interface provided by the operating system, otherwise your program will lose the cross platform feature.

ctypes official documents

Correspondence between Python type and C language type:

This table lists the correspondence of basic data among ctypes, c and python. When defining the parameters and return values of functions, you should remember:

1. The data type of ctypes must be used.
2. The parameter type is defined by the keyword argtypes, and the return type is defined by restype. Argtypes must be a sequence, such as tuple or list, or an error will be reported.
3. If the parameter type and return type are not explicitly defined, python defaults to int. Python needs to specify the parameter type and return value type of the function when calling a function in the dynamic library. Through objdll_ FuncPtr. Restype to specify the return value type of the dynamic library function through objdll_ FuncPtr. Argtypes to specify the parameter type of the dynamic library function, objdll_ FuncPtr. Argtypes is of type turple and contains the parameter type list of dynamic library functions. The specified parameter type must be the ctypes type corresponding to the parameter type in C/C + +.

Well, let's look directly at the example of Python calling dynamic library.

import platform
import ctypes

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)


if __name__ == '__main__':
    
    ## Call print_hello
    objDll.print_hello()

The operation results are as follows:

The result is the same as using C.

The following explains how to use Python instead of dynamic libraries with different parameter types.


2.1 value type

For the dynamic library function whose parameter type and return value type are value types, the operation is relatively simple. You only need to specify the corresponding parameter and return value ctype type to call.

C/C + + Code:

class Math {

public:
	int add(int a, int b);

};

int Math::add(int a, int b)
{
	return a + b;
}

External "C" is required for C + + function calls, that is, only C functions can be called, methods cannot be called directly, but C + + methods can be parsed. Instead of using extern "C", the built dynamic link library does not have the symbol table of these functions.

//C math
Math mathObj;
DLL_API int add(int a, int b)
{
	return mathObj.add(a, b);
}

Python code:

import platform
import ctypes

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

if __name__ == '__main__':
    
    # Define function parameters
    nA = ctypes.c_int(2)
    nB = ctypes.c_int(3)
    # Specifies the return value type of the function
    objDll.add.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.add.argtypes = (ctypes.c_int, ctypes.c_int, )
    
    # Call function
    res = objDll.add(nA, nB)
    print('C : sum = ', res)

The operation results are as follows:

For simplicity, the following code will be written in C.



2.2 pointer type

To create a pointer of ctypes type, three related functions are required:

Byref is equivalent to the address symbol of C. when passing parameters, you can pass the pointer of the function through byref. The difference between pointer and pointer is that pointer returns an instance while pointer returns a type.

[example 1]
C/C + + Code:

DLL_API int max(uint32_t a, uint32_t b, uint32_t *maxNum)
{
	*maxNum = a > b ? a : b;

	return 0;
}

Python code:

import platform
import ctypes

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

if __name__ == '__main__':
    
    # Define function parameters
    nA = ctypes.c_uint32(2)
    nB = ctypes.c_uint32(3)
    
    nMax = ctypes.c_uint32(0)
    
    # Specifies the return value type of the function
    objDll.max.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.max.argtypes = (ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
    
    # Call function
    res = objDll.max(nA, nB, ctypes.byref(nMax))
    
    maxValue = nMax.value
    
    print('C : max = ', maxValue)

The operation results are as follows:

[example 2] sum
C/C + + Code:

//DLL_API int sum(uint32_t nArr[], uint32_t nLength, uint32_t *nSum)
DLL_API int sum(uint32_t *nArr, uint32_t nLength, uint32_t *nSum)
{
	if (nArr == nullptr)
	{
		return -1;
	}
	uint32_t i;
	*nSum = 0;

	for (i = 0; i < nLength; i++)
	{
		*nSum += nArr[i];
	}

	return 0;
}

Python code:

import platform
import ctypes
import numpy as np

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

if __name__ == '__main__':
    
    data = np.array([[0, 1, 2, 3]], dtype=np.uint32)
    
    # Define function parameters
    nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
    
    nLength = ctypes.c_uint32(4)
    
    nSum = ctypes.c_uint32(0)
    
    # Specifies the return value type of the function
    objDll.sum.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.sum.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
    
    # Call function
    res = objDll.sum(nArr, nLength, ctypes.byref(nSum))
    
    sumValue = nSum.value
    
    print('C : sum = ', sumValue)

The operation results are as follows:

When Python calls the sum function, the first parameter needs to pass a pointer, and the following statement returns the address of the pointer:

nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))

Of course, there are the following ways:

import platform
import ctypes
import numpy as np

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)


if __name__ == '__main__':
    
    data = np.array([[0, 1, 2, 3]], dtype=np.uint32)
    
    # Define function parameters
    nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
    
    print('nArr : ', nArr)
    nLength = ctypes.c_uint32(4)
    
    nSum = ctypes.c_uint32(0)
    
    # Specifies the return value type of the function
    objDll.sum.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.sum.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
    
    # Call function
    res = objDll.sum(ctypes.byref(nArr), nLength, ctypes.byref(nSum))
    
    sumValue = nSum.value
    
    print('C : sum = ', sumValue)

The operation results are as follows:

[example 3]
C/C + + Code:

//sort
DLL_API int bubble_sort(uint32_t *nOldArr, uint32_t nLen, uint32_t *nNewArr)
{
	int i, j, temp;

	for (i = 0; i < nLen; i++)
	{
		nNewArr[i] = nOldArr[i];
	}

	for (i = 0; i < nLen - 1; i++)
	{
		for (j = 0; j < nLen - 1 - i; j++)
		{
			if (nNewArr[j] > nNewArr[j + 1])
			{
				temp = nNewArr[j];
				nNewArr[j] = nNewArr[j + 1];
				nNewArr[j + 1] = temp;
			}
		}
	}
	return 0;
}

Python code:

import platform
import ctypes
import numpy as np

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

if __name__ == '__main__':
    
    data = np.array([[10, 2, 15, 3, 56]], dtype=np.uint32)
    
    # Define function parameters
    nOldArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
    
    print('nOldArr : ', nOldArr)
    
    nLen = ctypes.c_uint32(5)
    
    nNewArr = (5 * ctypes.c_uint32)()
    
    # Specifies the return value type of the function
    objDll.bubble_sort.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.bubble_sort.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
    
    # Call function
    res = objDll.bubble_sort(ctypes.byref(nOldArr), nLen, nNewArr)
    
    print(nNewArr[0], ' ', nNewArr[1], ' ', nNewArr[2], ' ', nNewArr[3], ' ', nNewArr[4])

The operation results are as follows:

The last parameter of C code is an array. Python needs to build an array and pass its address to c function.

2.3 structure type

[example 1]
C/C + + Code:

//structural morphology
typedef struct {
	int nX;
	int nY;
	int nZ;
}STPoint;

DLL_API int sum_square(STPoint *stPoint, uint32_t *nSum)
{
	if (stPoint == nullptr)
	{
		return -1;
	}

	*nSum = ((stPoint->nX) * (stPoint->nX) + (stPoint->nY) * (stPoint->nY) + (stPoint->nZ) * (stPoint->nZ));

	return 0;
}

Python code:

import platform
import ctypes

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

class STPoint(ctypes.Structure):
    _fields_ = [("nX", ctypes.c_int), ("nY", ctypes.c_int), ("nZ", ctypes.c_int)] 

if __name__ == '__main__':
    
    stPoint = STPoint()
    
    stPoint.nX = 2
    stPoint.nY = 3
    stPoint.nZ = 4
    
    nSum = ctypes.c_uint32()
    
    # Specifies the return value type of the function
    objDll.sum_square.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.sum_square.argtypes = (ctypes.POINTER(STPoint), ctypes.POINTER(ctypes.c_uint32), )
    
    # Call function
    res = objDll.sum_square(ctypes.byref(stPoint), ctypes.byref(nSum))
    
    sumSquare = nSum.value
    
    print('C : sum_square = ',sumSquare)

The operation results are as follows:

[example 2]
C/C + + Code:

typedef struct {
	int nMax;
	int nMin;
}STMaxMin;

DLL_API int max_min(int *nArr, uint32_t nLength, STMaxMin *stMaxMin)
{
	if (nArr == nullptr)
	{
		return -1;
	}
	int i;
		
	stMaxMin->nMax = nArr[0];
	stMaxMin->nMin = nArr[0];

	for (i = 0; i < nLength; i++) 
	{
		if (nArr[i] > stMaxMin->nMax)
		{
			stMaxMin->nMax = nArr[i];
		}

		if (nArr[i] < stMaxMin->nMin)
		{
			stMaxMin->nMin = nArr[i];
		}

	}

	return 0;
}

Python code:

import platform
import ctypes
import numpy as np

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]

# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

class STMaxMin(ctypes.Structure):
    _fields_ = [("nMax", ctypes.c_int), ("nMin", ctypes.c_int)] 

if __name__ == '__main__':
    
    data = np.array([[10, 2, 15, 3, 56]], dtype=np.int)
    
    nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_int))
    
    nLength = ctypes.c_uint32(5)
        
    stMaxMin = STMaxMin()
    
    # Specifies the return value type of the function
    objDll.max_min.restype = ctypes.c_int
    
    # Specifies the parameter type of the function
    objDll.max_min.argtypes = (ctypes.POINTER(ctypes.c_int), ctypes.c_uint32, ctypes.POINTER(STMaxMin), )
    
    # Call function
    res = objDll.max_min(nArr, nLength, ctypes.byref(stMaxMin))
    

    print('C : max =  ',stMaxMin.nMax, 'min = ',stMaxMin.nMin )

The operation results are as follows:

The general steps to call dll in Python are:

1. Use extern c keyword and__ declspec(dllexport) and__ declspec(dllimport) wraps the dll.
2. Use ctypes library to load dynamic library.
3. Specify the return value type and parameter type of the dynamic library function according to the corresponding relationship between the data type in C code and the type in cttypes.
4. Call functions in the dynamic library.
5. Convert the return result of the function to the type in python for use.

[note] there is no reference type in C language. If the dynamic library is a function written in C + + with reference type parameters, it needs to be wrapped into C dynamic library with pointer type first.



3 speed comparison between Python and C/C + +

Many friends will say that it's unnecessary for Python to call the C library. It's better to write directly in Python or directly in C. Before answering this question, here is an example to illustrate the previous sorting algorithm.
Look directly at the code:

# -*- coding: utf-8 -*-
"""
@file                main.py
@author              BruceOu
@version             V1.0
@date                2021-08-06
@blog                https://blog.bruceou.cn/
@Official Accounts   Embedded experimental building
@brief               main
"""
import platform
import ctypes
import numpy as np
import time 

suffixNmae = {
    "Windows":".dll",
    "Linux":".so"
}

libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]


def bubbleSort(arr):
    n = len(arr)
 
    # Traverse all array elements
    for i in range(n):
 
        # Last i elements are already in place
        for j in range(0, n-i-1):
 
            if arr[j] > arr[j+1] :
                arr[j], arr[j+1] = arr[j+1], arr[j]
                
# Load dynamic library
objDll = ctypes.cdll.LoadLibrary(libPath)

isCpp = False

if __name__ == '__main__':
    
    arr = np.random.randint(low=1, high=100000, size=10000, dtype=np.uint32)
    
    data = np.array(arr, dtype=np.uint32)
    
    start = time.time()
     
    if(isCpp):
        length = data.size
        
        # Define function parameters
        nOldArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
        
        nLen = ctypes.c_uint32(length)
        
        nNewArr = (length * ctypes.c_uint32)()
        
        # Specifies the return value type of the function
        objDll.bubble_sort.restype = ctypes.c_int
        
        # Specifies the parameter type of the function
        objDll.bubble_sort.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
    
        # Call function
        res = objDll.bubble_sort(ctypes.byref(nOldArr), nLen, nNewArr)
        
        print(nNewArr[0], ' ', nNewArr[1], ' ', nNewArr[2], ' ', nNewArr[3], ' ', nNewArr[4])
        
    else:
        bubbleSort(arr)    
        print(arr[0], ' ', arr[1], ' ', arr[2], ' ', arr[3], ' ', arr[4])
    
    end = time.time()
    
    print('time : ', end- start)

Look at the results of two language implementations:

C library [isCpp=True]

The results of the above two times are about 0.0937s. Let's take a look at the time-consuming Python algorithm.

Python[isCpp=False]

It takes about 24s.

It can be seen that they are not of an order of magnitude, and the higher the computational complexity. But why use Python again? Python is simple and has many libraries. It takes a little time to write code, and the running time can be optimized by means of parallel GPU. Therefore, when we improve the running speed of code and want to do some logic code that is difficult to implement in C, we can use C to write algorithm and implement logic in Python, and many Python libraries are written in C.


Welcome to my website

Bruce Ou's beep beep beep
Bruce Ou's home page
Bruce Ou's blog
Bruce Ou's CSDN blog
Bruce Ou's short book

Welcome to subscribe to my WeChat official account.

Keywords: Python dll

Added by James Bond 007 on Sat, 25 Dec 2021 03:46:20 +0200