13

I am having trouble making a simple "Hello World" python function which I can call from a C program.

Here is the contents of my helloworld.py file:

def hw():
  print("Hello World")

Here is the contents of the caller.pyx file:

from helloworld import hw

cdef public void call_hw():
  hw()

And here is the contents of my main.c file:

#include <Python.h>
#include "caller.h"

int
main()
{
  Py_Initialize();
  call_hw();
  Py_Finalize();
}

Here are the commands I do:

> cython caller.pyx
> gcc -g -Wall -I/usr/include/python3.12 -c caller.c
> gcc -g -Wall -I/usr/include/python3.12 -c main.c
> gcc -g -Wall -I/usr/include/python3.12 -o main *.o -lpython3.12
> ./main
Segmentation fault (core dumped)

Here is a backtrace:

Program received signal SIGSEGV, Segmentation fault.
0x0000555555558898 in __Pyx__GetModuleGlobalName (name=0x0) at caller.c:2739
2739        result = _PyDict_GetItem_KnownHash(__pyx_d, name, ((PyASCIIObject *) name)->hash);
(gdb) bt
#0  0x0000555555558898 in __Pyx__GetModuleGlobalName (name=0x0) at caller.c:2739
#1  0x0000555555556d9a in call_hw () at caller.c:2002
#2  0x000055555555cef6 in main () at main.c:8

Can anyone tell me what I'm doing wrong?

1

2 Answers 2

13

caller is still a Python module, and it needs to be initialised and imported before you can use it or any of its functions. That is, you need to ensure the from helloworld import hw bit of python code is run. See the documentation on embedding for more details. Further documentation on using Cython Declarations from C can be found here.

The key additions you need are:

  • call PyImport_AppendInittab
  • tell Python where it can import modules from (see end)
  • call PyImport_ImportModule

For example, main.c would become:

#include <Python.h>
#include <stdio.h>
#include "caller.h"

int
main()
{
    PyObject *pmodule;
    /* Add a built-in module, before Py_Initialize
       PyInit_caller is generated code from caller.c
     * NB.the name will vary with the module name */
    if (PyImport_AppendInittab("caller", PyInit_caller) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }

    /* Initialize the Python interpreter.  Required.
       If this step fails, it will be a fatal error. */
    Py_Initialize();

    /* Optionally set import path here */
    // PyRun_SimpleString("import sys\nsys.path.insert(0,'')");

    /* Import the module.
       If this step fails, it will be a fatal error. */
    pmodule = PyImport_ImportModule("caller");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'caller'\n");
        goto exit_with_error;
    }

    /* your code goes here */    
    call_hw();

    /* end of program */
    Py_Finalize();
    return 0;

    /* Clean up in the error cases above. */
exit_with_error:
    Py_Finalize();
    return 1;
}

PYTHONPATH

However, that is not all you need to do. You will also need to tell the interpreter where it can look when importing modules. By default, the current working directory is added when running normal python scripts. This is not the case for embedded scripts. Instead you will need to manually do this. One simple way is to set the environment variable PYTHONPATH to .. For instance:

PYTHONPATH=. ./main

Alternatively, you can put the following before your first module import (but after you initialise the interpreter) in main.c:main()

PyRun_SimpleString("import sys\nsys.path.insert(0,'')");
3
  • 2
    You proposal works for me, contrarily to the first answer, but I see yours too late and then in my own answer I did without using cython I supposed to be the problem.
    – bruno
    Commented yesterday
  • The docs seem to say that importing the module from the C side is optional, though the example therein does do it. They agree with you about PyImport_AppendInittab() being needed. It would perhaps be worthwhile to note that Cython can generate an appropriate main() function to use as an example / starting point. Commented 14 hours ago
  • It doesn't say it is optional. It says it is optional at that point in the program, and can be deferred until such time as the module is needed. I first thought that when I read that comment. But look at it again, and you'll see it's about the when of the import not the if. The "Not importing the Cython module" section of the link makes this clear: "[cython] creates C code that’s designed to be imported as a Cython module" and "Therefore, if you decide to skip the initialization and just go straight to running your public functions you will likely experience crashes"
    – Dunes
    Commented 13 hours ago
7

Unfortunately the proposed first answer made by @Roan does not work for me, and the execution fail.

Before to see the second answer made by @Dunes which works for me, I supposed the problem comes from cython, even I do cython3 -3 caller.pyx (strangely -2 by default), and I followed An Easy Guide to Understanding PyImport_ImportModule in Python, without any use of cython.

I give it in case to not use cython interests someone.

I am on a Pi5 Debian 12 (Bookworm) with Python 3.11

main.c is now :

#include <Python.h>

int main() {
    // Initialize the Python interpreter
    Py_Initialize();

    // Add the current directory to the Python module search path
    PyRun_SimpleString("import sys\n"
                       "sys.path.append('.')\n");

    // Import the Module
    PyObject* pModule = PyImport_ImportModule("helloworld");
    
    if (pModule == 0)
      PyErr_Print();
    else {
      // Use the Module to access the function
      PyObject* pFunc = PyObject_GetAttrString(pModule, "hw");
      
      if (pFunc && PyCallable_Check(pFunc)) {
        PyObject_CallFunction(pFunc, "()");
        Py_XDECREF(pFunc);
        Py_XDECREF(pModule);
      }
      else
        PyErr_Print();
    }

    // Clean up
    Py_Finalize();
    return 0;
}

helloworld.py is unchanged, caller.pyx is not relevant and does not need to exist

Compilation and execution :

bruno@raspberrypi:/tmp $ gcc -g -Wall -I/usr/include/python3.11 -o main main.c -lpython3.11
bruno@raspberrypi:/tmp $ ./main 
Hello World
bruno@raspberrypi:/tmp $ 

There is no memory leaks but some reachable allocated memory is still not free (accessible from global vars then) :

bruno@raspberrypi:/tmp $ valgrind --leak-check=full  ./main 
==31826== Memcheck, a memory error detector
==31826== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==31826== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==31826== Command: ./main
==31826== 
Hello World
==31826== 
==31826== HEAP SUMMARY:
==31826==     in use at exit: 408,136 bytes in 15 blocks
==31826==   total heap usage: 1,595 allocs, 1,580 frees, 2,871,796 bytes allocated
==31826== 
==31826== LEAK SUMMARY:
==31826==    definitely lost: 0 bytes in 0 blocks
==31826==    indirectly lost: 0 bytes in 0 blocks
==31826==      possibly lost: 0 bytes in 0 blocks
==31826==    still reachable: 408,136 bytes in 15 blocks
==31826==         suppressed: 0 bytes in 0 blocks
==31826== Reachable blocks (those to which a pointer was found) are not shown.
==31826== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==31826== 
==31826== For lists of detected and suppressed errors, rerun with: -s
==31826== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
bruno@raspberrypi:/tmp $ 

So the second answer works for me, and for information with it I still have no memory leaks but still reachable allocated memory :

bruno@raspberrypi:/tmp $ cython3 -3 caller.pyx 
/usr/lib/python3/dist-packages/pythran/tables.py:4530: FutureWarning: In the future `np.bool` will be defined as the corresponding NumPy scalar.
  if not hasattr(numpy, method):
/usr/lib/python3/dist-packages/pythran/tables.py:4563: FutureWarning: In the future `np.bytes` will be defined as the corresponding NumPy scalar.
  obj = getattr(themodule, elem)
bruno@raspberrypi:/tmp $ gcc -g -Wall -I/usr/include/python3.11 -o main caller.c main.c -lpython3.11
bruno@raspberrypi:/tmp $ 
bruno@raspberrypi:/tmp $ ./main 
Hello World
bruno@raspberrypi:/tmp $ 
bruno@raspberrypi:/tmp $ valgrind --leak-check=full  ./main 
==31666== Memcheck, a memory error detector
==31666== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==31666== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==31666== Command: ./main
==31666== 
Hello World
==31666== 
==31666== HEAP SUMMARY:
==31666==     in use at exit: 409,145 bytes in 17 blocks
==31666==   total heap usage: 1,598 allocs, 1,581 frees, 2,871,853 bytes allocated
==31666== 
==31666== LEAK SUMMARY:
==31666==    definitely lost: 0 bytes in 0 blocks
==31666==    indirectly lost: 0 bytes in 0 blocks
==31666==      possibly lost: 0 bytes in 0 blocks
==31666==    still reachable: 409,145 bytes in 17 blocks
==31666==         suppressed: 0 bytes in 0 blocks
==31666== Reachable blocks (those to which a pointer was found) are not shown.
==31666== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==31666== 
==31666== For lists of detected and suppressed errors, rerun with: -s
==31666== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
bruno@raspberrypi:/tmp $ 
2
  • I wouldn't worry too much about trying to run Python through valgrind. Clearing up reliably at the end of Python is generally tricky, and not really very meaningful. I agree with your conclusion that Cython isn't adding much to what the original poster is trying to do right now
    – DavidW
    Commented yesterday
  • @DavidW The documentation of Py_Finalize() (in fact of Py_FinalizeEx() which is the real function) says Ideally, this frees all memory allocated by the Python interpreter, I wanted to look because ideally is not very strong ;-) Apart of that, I did because this is also a pretext to speak about valgrind which is a very powerful tool for people programming in C or C++, and not only about memory leaks, for me to use it is a good reflex including on programm (apparently ?) working well.
    – bruno
    Commented yesterday

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.