65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C/C++ 中嵌入 Python:第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (19投票s)

2005 年 10 月 2 日

CPOL

7分钟阅读

viewsIcon

132445

downloadIcon

3316

本文讨论了如何在 C/C++ 应用程序中嵌入 Python 模块的一些高级主题。

引言

这是系列文章的第二部分。 第一部分 介绍了 Python/C API,这是一个 C 库,有助于将 Python 模块嵌入到 C/C++ 应用程序中。基本上,API 库是一组 C 例程,用于初始化 Python 解释器、调用您的 Python 模块并完成嵌入。在第一部分中,我演示了如何调用 Python 模块中定义的函数、类和方法。然后,我们讨论了多线程嵌入的详细信息,这是 C/C++ 程序员在集成阶段通常会遇到的一个问题。在讨论中提出了一个问题:当 C/C++ 代码与嵌入的 Python 模块在不同的线程/进程上运行时,它们如何通信?请参阅文章: “在 C/C++ 中嵌入 Python:第一部分”

我将探讨此问题的替代解决方案,特别是进程间通信 (IPC) 机制。与第一部分一样,本文不会系统地教授 Python 语言,而是会描述 Python 代码在出现时的工作原理。讨论将集中在如何将 Python 模块与您的 C/C++ 应用程序集成。我将采用与第一部分相同的实践方法,但在此我将提供一些有限的理论讨论,这得益于本部分主题的性质。例如,共享内存的主题值得一些理论。尽管如此,我还是会将大部分讨论留给 Jeffrey Richter 的经典著作。

同样,我提供的源代码是可移植的,这意味着它旨在同时在 Windows 和 Linux 上运行。为了使用源代码,您应该安装最新的 Python 发行版、Visual C++(或 Linux 上的 GCC 编译器)。我用来测试的环境是:Python 2.4(Windows 和 Linux)、Visual C++ 6.0(Windows)或 GCC 3.2(RedHat 8.0 Linux)。使用 Visual C++ 时,请选择 Release 配置进行构建,因为 Debug 配置需要 Python 调试库“python24_d.lib”,而该库并未随普通发行版提供。

背景

Python 是一种强大的解释型语言,就像 Java、Perl 和 PHP 一样。它支持程序员期望的许多出色功能,我最喜欢的两个功能是“简单”和“可移植”。结合可用的工具和库,Python 是建模和仿真开发人员的理想语言。最重要的是,它是免费的,为 Python 程序员编写的工具和库也是免费的。有关该语言的更多详细信息,请访问官方 网站

用于嵌入的 TCP/IP 套接字

Python 实现了多种 IPC,包括套接字、内存映射文件 (MMAP)、队列、信号量、事件、锁、互斥锁等。我们将在以下内容中研究两种 IPC:TCP/IP 套接字和 MMAP。本节讨论一个简单的 TCP/IP 客户端/服务器模型。下一节将介绍 C/C++ 和 Python 模块如何使用 MMAP 相互通信。

让我们从一个简单的应用程序开始,该应用程序在 C 代码中实现了一个 TCP 客户端,以与 Python 模块中的 TCP 服务器进行通信。这是完整的源代码“call_socket.c

// call_socket.c - A sample program to 
// demonstrate the TCP client
//
#ifdef WIN32
#include <sys/types.h>
#include <Winsock2.h>
#define    WINSOCKVERSION    MAKEWORD( 2,2 )        
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>
#endif

#include <stdio.h>
#include <string.h>

#define MAX_BUFFER    128
#define HOST        "127.0.0.1"
#define PORT         50007

int main ()
{
  int connectionFd, rc, index = 0, limit = MAX_BUFFER;
  struct sockaddr_in servAddr, localAddr;
  char buffer[MAX_BUFFER+1];

#ifdef WIN32
  // Start up WinSock2
  WSADATA wsaData;
  if( WSAStartup( WINSOCKVERSION, &wsaData) != 0 ) 
     return ERROR;        
#endif

  memset(&servAddr, 0, sizeof(servAddr));
  servAddr.sin_family = AF_INET;
  servAddr.sin_port = htons(PORT);
  servAddr.sin_addr.s_addr = inet_addr(HOST);

  // Create socket
  connectionFd = socket(AF_INET, SOCK_STREAM, 0);

  /* bind any port number */
  localAddr.sin_family = AF_INET;
  localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  localAddr.sin_port = htons(0);
  
  rc = bind(connectionFd, 
      (struct sockaddr *) &localAddr, sizeof(localAddr));

  // Connect to Server
  connect(connectionFd, 
      (struct sockaddr *)&servAddr, sizeof(servAddr));

  // Send request to Server
  sprintf( buffer, "%s", "Hello, Server!" );
  send( connectionFd, buffer, strlen(buffer), 0 );
 
  printf("Client sent to sever %s\n", buffer);

  // Receive data from Server
  sprintf( buffer, "%s", "" );
  recv(connectionFd, buffer, MAX_BUFFER, 0);
  printf("Client read from Server %s\n", buffer);

#ifdef WIN32
  closesocket(connectionFd);
#else
  close(connectionFd);
#endif
  
  printf("Client closed.\n");

  return(0);
}

Python 源代码文件“py_socket_server.py”如下

'''A sample of Python TCP server'''

import socket

HOST = '127.0.0.1'        # Local host
PORT = 50007              # Arbitrary port

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)

print 'Waiting for connection...'
conn, addr = s.accept()

print 'Connected by client', addr
while 1:
    data = conn.recv(1024)
    if not data: break
    print 'Received data from client', 
               repr(data), '...send it back...'
    conn.send(data)

conn.close()
print 'Server closed.'

在 Windows 上,只需编译 C 源文件并获取可执行文件,我们称之为“call_socket.exe”。要测试此 IPC,请打开一个命令窗口并启动 Python。然后导入“py_socket_server”以启动 Python TCP 服务器。打开另一个命令窗口,运行“call_socket.exe”。您将在两个窗口中看到以下输出

Python 2.4.1 (#65, Mar 30 2005, 09:13:57) 
         [MSC v.1310 32 bit (Intel)] on win32
Type "help", "copyright", 
  "credits" or "license" for more information.
>>> import py_socket_server
Waiting for connection...
Connected by client ('127.0.0.1', 1482)
Received data from client 'Hello, 
              Server!' ...send it back...
Server closed.
>>>
C:\embedpython_2\embedpython_2_demo>call_socket
Client sent to sever "Hello, Server!"
Client read from Server "Hello, Server!"
Client closed.

C:\embedpython_2\embedpython_2_demo>

C 代码可以在 Windows 和 Linux 平台上运行。通过删除可移植性可以进一步简化它。请注意,为简洁起见,已省略对返回值的有效性检查。C 源代码本身易于理解,除了我们必须将本地地址绑定到客户端,这在客户端通常是不必要的。然而,在此集成中,如果没有绑定,Python 服务器会报告以下错误

>>> import py_socket_server
Waiting for connection...
Connected by client ('127.0.0.1', 1479)
Received data from client Hello, Server! ...send it back...
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "py_socket_server.py", line 16, in ?
    data = conn.recv(1024)
socket.error: (10054, 'Connection reset by peer')

您可能想看看 Python TCP 客户端是什么样子。好吧,这是 Python 源代码“py_socket_client.py”。它简单而干净。更重要的是,它可以在 Windows 和 Linux 之间移植!

'''A sample of Python TCP client'''

import socket

HOST = '127.0.0.1' # The localhost
PORT = 50007    # The same port as used by the server

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

print 'Send to server', '\'Hello, world\''
s.send('Hello, world')

data = s.recv(1024)
print 'Received back', repr(data)

s.close()
print 'Client closed.'

在多线程嵌入中使用上述客户端/服务器模型相当容易。通常,在单独的线程上运行 Python 模块时,我们有两种选择

  1. 如果您在 Python 模块内部创建线程,请将服务器代码放在 Python 线程函数中。
  2. 如果您在 C/C++ 代码中创建线程,请在 C 线程函数中调用 Python 服务器代码。

我将这个问题留给读者练习。有关这两种方法的更多详细信息,请参阅本文系列的第一部分。

Python 和 C/C++ 的共享内存 (MMAP)

首先,为本节做一些理论准备。在类 Unix 系统(如 GNU/LINUX)中,共享内存段和内存映射文件 (MMAP) 是两个不同的概念。MMAP 是内存映射文件 I/O。您可以使用 MMAP 作为 IPC,但效率不高,因为它会从每个进程的内存空间复制到磁盘文件。相比之下,共享内存段是一种更快的 IPC 形式,因为进程可以在各自的地址空间中共享内存段。没有磁盘复制或内存移动。

Windows 以一种略有不同的方式实现了内存映射文件(称为“MMF”)。MMF 可以由用户定义的磁盘文件或系统页面文件支持。当 MMF 用作 IPC 时,Windows 会创建一个命名的文件映射(内核)对象。通过内核对象,进程可以映射到同一个磁盘文件。这与 MMAP 相同。但是,当 MMF 由分页支持时,这种 IPC 可以非常高效。因为如果您有足够的物理内存,就不会执行分页。它就变成了一个共享内存段。Windows 实际上将 MMAP 和共享内存段统一在 MMF 的同一框架下!有关 Windows 实现的更多详细信息,请参阅 Jeffrey Richter 的“Programming Applications for Microsoft Windows”。

现在让我们考虑以下场景。有人编写了一个 Python 模块,该模块 intended to run on a separate thread/process(意图在单独的线程/进程上运行)。它定义了一个 MMAP 接口,通过 MMAP 与该模块的用户进行通信。当我们将它与我们的 C/C++ 应用程序集成时,我们会为它设置 MMAP 接口,然后启动它的执行。我们在客户端的实现位于“call_mmap.c”。这是完整的源代码

// call_mmap.c - A sample of python embedding 
// (calling python functions 
// from within C code)
#include <Python.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>

#ifdef WIN32    // Windows includes
#include <Windows.h>
#include <process.h>
#define sleep(x) Sleep(1000*x)
HANDLE hFile, handle, map;
#else        // POSIX includes
#include <pthread.h>
#include <sys/mman.h>
pthread_t mythread;
#endif

void myThread(void*);

#define NUM_ARGUMENTS 5
typedef struct 
{
   int argc;
   char *argv[NUM_ARGUMENTS]; 
} CMD_LINE_STRUCT;

int main(int argc, char *argv[])
{
    int i;
    char* indata = NULL;
    CMD_LINE_STRUCT cmd;
    
    cmd.argc = argc;
    for( i = 0; i < NUM_ARGUMENTS; i++ )
    {
        cmd.argv[i] = argv[i];
    }

    if (argc < 4) 
    {
        fprintf(stderr, "Usage: " + 
          "exe_name python_file class_name function_name\n");
        return 1;
    }

    /////////////////////////////////////////////////////////
    // Create a MMAP
    /////////////////////////////////////////////////////////
#ifdef WIN32
    // Create a memory-mapped file (MMF)
    hFile = CreateFile((LPCTSTR) "input.dat", 
        GENERIC_WRITE | GENERIC_READ, 
        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, 
        OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    
    if(hFile == INVALID_HANDLE_VALUE) 
    {
    // Failed to create file 
    return 1;
    }

    map = CreateFileMapping(hFile, NULL, 
             PAGE_READWRITE, 0, 1024, "MMAPShmem");
    indata = (char *) MapViewOfFile (map, 
             FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); 
#else
    int fd;
    if((fd = open("input.dat", O_RDWR)) == -1)
    {
        printf("Couldn't open 'input.data'\n");
    }
    indata = mmap(    NULL, 1024, 
        PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
#endif

    if(indata != NULL)
    {    
        printf("Wrapper has created a MMAP " + 
                         "for file 'input.data'\n");
    }
 
    ///////////////////////////////////////////////
    // Create a thread
    ///////////////////////////////////////////////
#ifdef WIN32
    // Windows code
    handle = (HANDLE) _beginthread( myThread, 0, &cmd);
#else
    // POSIX code
    pthread_create( &mythread, NULL, myThread, 
                                         (void*)&cmd );
#endif

    // Random testing code
    for(i = 0; i < 10; i++)
    {
    memset(indata, 0, 1024);
    sprintf(indata, "%d", i);
    indata[3] = '\n';
      printf("The Main thread has writen %d to MMAP.\n", i);
    sleep(1);
    }

    printf("Main thread waiting for " + 
                "Python thread to complete...\n");

    // Join and wait for the created thread to complete...
#ifdef WIN32
    // Windows code
    WaitForSingleObject(handle,INFINITE);
    // Clean up
    UnmapViewOfFile(indata);
    CloseHandle(map);
    CloseHandle(hFile);
#else
    // POSIX code
    pthread_join(mythread, NULL);
    // Clean up
    munmap(indata, 1024);
    close(fd);
#endif
    
    printf("Main thread finished gracefully.\n");
    return 0;
}

void myThread( void *data )
{
    PyObject *pName, *pModule, *pDict, 
                            *pClass, *pInstance;
    PyThreadState *mainThreadState, 
                     *myThreadState, *tempState;
    PyInterpreterState *mainInterpreterState;
    
    CMD_LINE_STRUCT* arg = (CMD_LINE_STRUCT*)data;

    // Initialize python inerpreter
    Py_Initialize();
        
    // Initialize thread support
    PyEval_InitThreads();

    // Save a pointer to the main PyThreadState object
    mainThreadState = PyThreadState_Get();

    // Get a reference to the PyInterpreterState
    mainInterpreterState = mainThreadState->interp;

    // Create a thread state object for this thread
    myThreadState = PyThreadState_New(mainInterpreterState);

    // Swap in my thread state
    tempState = PyThreadState_Swap(myThreadState);

    // Now execute some python code (call python functions)
    pName = PyString_FromString(arg->argv[1]);
    pModule = PyImport_Import(pName);

    // pDict and pFunc are borrowed references 
    pDict = PyModule_GetDict(pModule);

    // Build the name of a callable class 
    pClass = PyDict_GetItemString(pDict, arg->argv[2]);

    // Create an instance of the class
    if (PyCallable_Check(pClass))
    {
    pInstance = PyObject_CallObject(pClass, NULL); 
    }

    // Call a method of the class with no parameters
    PyObject_CallMethod(pInstance, arg->argv[3], NULL);

    // Swap out the current thread
    PyThreadState_Swap(tempState);

    // Clean up thread state
    PyThreadState_Clear(myThreadState);
    PyThreadState_Delete(myThreadState);

    // Clean up
    Py_DECREF(pModule);
    Py_DECREF(pName);

    Py_Finalize();
    printf("My thread is finishing...\n");

    // Exiting the thread
#ifdef WIN32
    // Windows code
    _endthread();
#else
    // POSIX code
    pthread_exit(NULL);
#endif
}

C 应用程序执行以下操作

  • 创建一个名为“input.dat”的磁盘文件。然后,将文件映射到内存空间。
  • 创建一个线程。线程函数执行 Python 模块。
  • C 应用程序线程写入 MMAP 文件十次。请注意,写入循环是在创建并运行 Python 线程之后开始的。

这是 Python 源代码“py_mmap.py

'''Python source designed to demonstrate '''
'''the use of python embedding'''

import sys
import os
import time
import mmap

INDATAFILENAME = 'input.dat'
LENGTHDATAMAP = 1024

class MMAPShmem:
    def run(self):
        inDataFile = open(INDATAFILENAME, 'r+')
        print 'inDataFile size: ', 
            os.path.getsize(INDATAFILENAME), 
            'MMAP size: ', LENGTHDATAMAP
        inDataNo = inDataFile.fileno() 
        
        inDataMap = mmap.mmap(inDataNo, LENGTHDATAMAP, 
                              access=mmap.ACCESS_WRITE) 
        inDataMap.seek(0)    # simple test of validity
    
        # write something into the mapped file
        x = 567
        inDataMap.write('%d' %x + '\n')

        for i in range(10):
            # read out from the file to verify
            inDataMap.seek(0)
            y = inDataMap.readline()
            print 'Python thread read from MMAP:', y 
            inDataMap.seek(0)
            inDataMap.write('%d' %x + '\n')
            print 'Python thread write back to MMAP:', x 
            time.sleep(1)

        inDataFile.close()

Python 模块“py_mmap”定义了一个类“MMAPShmem”,它有一个方法 run()。它所做的就是打开 C 代码创建的磁盘文件并将其映射到内存。然后,模块可以将映射的文件用作普通文件 I/O。在每个 for 循环中,Python 读取 MMAP 并打印其内容。然后,它会覆盖 MMAP。请注意,这十次读/写与主 C 线程的十次写入是并行运行的。

打开一个命令窗口并运行“call_mmap py_mmap MMAPShmem run”。您应该会看到如下所示的输出

Wrapper has created a MMAP for file 'input.data'
The Main thread has writen 0 to MMAP.
inDataFile size:  1024 MMAP size:  1024
Python thread read from MMAP: 567

Python thread write back to MMAP: 567
The Main thread has writen 1 to MMAP.
Python thread read from MMAP: 1

Python thread write back to MMAP: 567
The Main thread has writen 2 to MMAP.
Python thread read from MMAP: 2

Python thread write back to MMAP: 567
The Main thread has writen 3 to MMAP.
Python thread read from MMAP: 3

Python thread write back to MMAP: 567
The Main thread has writen 4 to MMAP.
Python thread read from MMAP: 4

Python thread write back to MMAP: 567
The Main thread has writen 5 to MMAP.
Python thread read from MMAP: 5

Python thread write back to MMAP: 567
The Main thread has writen 6 to MMAP.
Python thread read from MMAP: 6

Python thread write back to MMAP: 567
The Main thread has writen 7 to MMAP.
Python thread read from MMAP: 7

Python thread write back to MMAP: 567
The Main thread has writen 8 to MMAP.
Python thread read from MMAP: 8

Python thread write back to MMAP: 567
The Main thread has writen 9 to MMAP.
Python thread read from MMAP: 9

Python thread write back to MMAP: 567
Main thread waiting for Python thread to complete...
My thread is finishing...
Main thread finished gracefully.

显然,在两个单独线程上运行的 C 和 Python 代码通过 MMAP 文件“input.dat”进行通信。在这种情况下,由于我们使用了文本 I/O(与二进制 I/O 相比),您实际上可以检查内容。

关注点

我们的 MMAP 没有实现同步,这通常是共享内存数据保护所必需的。在实践中,您可能希望协调多个线程/进程对共享内存的访问。否则,无法保证独占性,并且您从共享内存中获得的数据可能是不可预测的。

结论

我们已经证明,我们可以有效地利用 Python/C API 将 Python 模块集成到我们的 C/C++ 应用程序中。我们的主要重点是如何在多线程应用程序中嵌入 Python。Python 和 C/C++ 模块之间的通信机制 IPC 已得到深入讨论。这是本文系列的结论部分。

历史

  • 这是本文和源代码的第一个修订版。
© . All rights reserved.