pythonc++python-c-api

Calling a function that execute Python in C++ from Python gives a free() invalid pointer error


I write a function that executes a Python string in C++ using the Python C API. The function works perfectly. When I run it, it executes the Python string.

The problem occurs when I make a .so file and then call the same function from the Python side using C++.

It gives a weird error. free() invalid pointer on Py_Initialize(); this line. When I comment down the line, then a segmentation fault occurs.


My runner.cpp code is:

#include <vector>
#include <dlfcn.h>
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <cstring>

typedef void (*Py_Initialize_t)();
typedef int (*PyRun_SimpleString_t)(const char *);
typedef void (*Py_Finalize_t)();


std::string execute_command(const char* command) {
    FILE* pipe = popen(command, "r");
    if (!pipe) {
        return "Error executing command";
    }

    std::ostringstream output;
    char buffer[128];
    while (!feof(pipe)) {
        if (fgets(buffer, 128, pipe) != nullptr) {
            output << buffer;
        }
    }

    // Close the pipe and return the output
    pclose(pipe);
    return output.str();
}

std::vector<std::string> split_string(const std::string& input, char delimiter) {
    std::vector<std::string> tokens;
    std::istringstream iss(input);
    std::string token;
    while (std::getline(iss, token, delimiter)) {
        tokens.push_back(token);
    }
    return tokens;
}

void execute_Prog730(std::string program) {
    std::cout << "Running Engine" << std::endl;
    std::string python_version_output = execute_command("python3 -c \"import sys; print(sys.version)\"");
    size_t start_pos = python_version_output.find_first_of("0123456789");
    size_t end_pos = python_version_output.find_first_not_of("0123456789.", start_pos);
    std::string python_version = python_version_output.substr(start_pos, end_pos - start_pos);
    std::vector<std::string> parts = split_string(python_version, '.');

    std::ostringstream lib_name_stream;
    lib_name_stream << "libpython" << parts[0] << '.' << parts[1] << ".so";
    std::string lib_name = lib_name_stream.str();
    std::cout << lib_name << std::endl;
    void* handle = dlopen(lib_name.c_str(), RTLD_LAZY);
    if(!handle && lib_name.back() == 'o') {
        std::string  lib_name_so1 = lib_name + ".1";
        handle = dlopen(lib_name_so1.c_str(), RTLD_LAZY);
    }

    if (!handle) {
        std::cerr << "Failed to load Python library" << std::endl;
        //return 1;
    }

    auto Py_Initialize = (Py_Initialize_t)dlsym(handle, "Py_Initialize");
    auto PyRun_SimpleString = (PyRun_SimpleString_t)dlsym(handle, "PyRun_SimpleString");
    auto Py_Finalize = (Py_Finalize_t)dlsym(handle, "Py_Finalize");

    if (!Py_Initialize || !PyRun_SimpleString || !Py_Finalize) {
        std::cerr << "Failed to resolve Python functions" << std::endl;
        dlclose(handle);
    }
    std::cout << "Debug" << std::endl;
    Py_Initialize();
    std::cout << "Debug" << std::endl;
    PyRun_SimpleString(program.c_str());
    Py_Finalize();
    dlclose(handle);
}

My runner.h file is:

#include <iostream>
#include "runner.cpp"

void execute_Prog730(std::string program);

My main.cpp file is:

#include <iostream>
#include <cstring>
#include <string.h>

#include <runner.h>

extern "C" void run(const char* program) {
    std::string pr(program);
    std::cout << pr << std::endl;
    execute_Prog730(pr);
}

The command I use to compile:

g++ -o main.so --shared -fPIC main.cpp -I/home/lakshit/Desktop/temp/include

Finally my main.py file is:

import ctypes

module = ctypes.CDLL('/home/lakshit/Desktop/temp/main.so')

func = module.run
func.argtypes = [ctypes.c_char_p]

func(b"print('hello')")

Note: I am using Ubuntu 22.04 (Jammy Jellyfish). Some versions require to add -ldl command to compile the .so file successfully.


Solution

  • To summarize the comment discussion:

    1. since Python is already loaded, you shouldn't call Py_Initialize and Py_Finalize
    2. since the Python DLL file is already loaded, you need to load it with RTLD_NOLOAD
    3. you need to acquire the GIL before you call Python from the C++ side, this is because ctypes.CDLL drops the GIL when calling C code.

    If you are calling a C function that you know will use the Python C API you should instead use ctypes.PyDLL which doesn't drop the GIL, and you won't need to re-acquire it from the C++ side.

    The following code solves the issue on my system.

    #include <vector>
    #include <dlfcn.h>
    #include <iostream>
    #include <sstream>
    #include <cstdlib>
    #include <cstring>
    
    namespace {
    
        enum PyGILState_STATE
        {
            locked, unlocked
        };
    
        using PyRun_SimpleString_t = int (*)(const char *);
        using PyGILState_Ensure_t = PyGILState_STATE (*)(void);
        using PyGILState_Release_t = void (*)(PyGILState_STATE);
    }
    
    std::string execute_command(const char* command) {
        FILE* pipe = popen(command, "r");
        if (!pipe) {
            return "Error executing command";
        }
    
        std::ostringstream output;
        char buffer[128];
        while (!feof(pipe)) {
            if (fgets(buffer, 128, pipe) != nullptr) {
                output << buffer;
            }
        }
    
        // Close the pipe and return the output
        pclose(pipe);
        return output.str();
    }
    
    std::vector<std::string> split_string(const std::string& input, char delimiter) {
        std::vector<std::string> tokens;
        std::istringstream iss(input);
        std::string token;
        while (std::getline(iss, token, delimiter)) {
            tokens.push_back(token);
        }
        return tokens;
    }
    
    void execute_Prog730(std::string program) {
        std::cout << "Running Engine" << std::endl;
        std::string python_version_output = execute_command("python3 -c \"import sys; print(sys.version)\"");
        size_t start_pos = python_version_output.find_first_of("0123456789");
        size_t end_pos = python_version_output.find_first_not_of("0123456789.", start_pos);
        std::string python_version = python_version_output.substr(start_pos, end_pos - start_pos);
        std::vector<std::string> parts = split_string(python_version, '.');
    
        std::ostringstream lib_name_stream;
        lib_name_stream << "libpython" << parts[0] << '.' << parts[1] << ".so";
        std::string lib_name = lib_name_stream.str();
        std::cout << lib_name << std::endl;
        void* handle = dlopen(lib_name.c_str(), RTLD_NOLOAD); // Use the already-loaded Python interpreter
        if(!handle && lib_name.back() == 'o') {
            std::string  lib_name_so1 = lib_name + ".1";
            handle = dlopen(lib_name_so1.c_str(), RTLD_NOLOAD);
        }
    
        if (!handle) {
            std::cerr << "Failed to load Python library" << std::endl;
            //return 1;
        }
    
        auto PyRun_SimpleString = (PyRun_SimpleString_t)dlsym(handle, "PyRun_SimpleString");
        auto PyGILState_Ensure = (PyGILState_Ensure_t)dlsym(handle, "PyGILState_Ensure");
        auto PyGILState_Release = (PyGILState_Release_t)dlsym(handle, "PyGILState_Release");
    
        if (!PyRun_SimpleString || !PyGILState_Ensure || !PyGILState_Release) {
            std::cerr << "Failed to resolve Python functions" << std::endl;
            if (handle)
            {
                dlclose(handle);
            }
        }
        std::cout << "Debug" << std::endl;
        auto state = PyGILState_Ensure(); // Acquire GIL
        PyRun_SimpleString(program.c_str());
        PyGILState_Release(state); // Release GIL
        if (handle)
        { dlclose(handle); }
    }
    
    print('hello')
    Running Engine
    libpython3.10.so
    Failed to load Python library
    Debug
    hello
    

    The Failed to load Python library happens because the main Python executable doesn't load libpython3, and instead statically links it, so those functions are loaded from the executable, not the shared library. If another version of Python dynamically links libpython3 then it will succeed.