Calling Into C or C++ Code From Python

Recently I wanted to call some C code from Python to check who was sending a particular signal. Apparently this functionality is only available as of Python 3 so I decided to write a custom module.

Doing so is relatively easy but all the documentation I found only was using
distutils to build the module and skipped over how to find the newly built
module. This post is mostly a write up of what I found out.

Compiling the Module

Compiling the Python module is easy. You just need to correct flags to pass to
the compiler. This information is easily accessible trough the python-config
utility.

Additionally you want to make a shared library. On macOS that means adding
-dynamiclib to the invocation.

clang `python-config --cflags` `python-config --ldflags` -dynamiclib mymodule.c -o mymodule.so

Make sure that the python-config in your path matches the python in your
path. For example on macOS, if you have installed python using Homebrew, you
might have a different Python in /usr/bin and /usr/local/bin.

Loading the Module

The most straightforward approach is just having the shared library living in
the current directory. All you need is import mymodule.

Python 2.7.10
Type "help", "copyright", "credits" or "license" for more information.
>>> import mymodule

Alternatively you can add the path where the modules lives to the
DYLD_LIBRARY_PATH or LD_LIBRARY_PATH for macOS or Linux respectively, or
more specifically the PYTHONPATH.

export DYLD_LIBRARY_PATH='/path/to/module':$DYLD_LIBRARY_PATH

Please note that the extension seems to matter. Giving the shared library the
usual .dylib extension on macOS did not work for me.

Python Module  

Below is a listing with the code for the Python module.

#include <Python.h>
#include <libproc.h>
#include <signal.h>

static void sighandler_handle(int signum, siginfo_t *siginfo, void *context) {
  int pid = getpid();
  int pgrp = getpgrp();
  int si_pid = (int)siginfo->si_pid;
  char si_path[PROC_PIDPATHINFO_MAXSIZE];

  // Print some info about the receiving and sending process.
  fprintf(stderr, "Signal received:\n");
  fprintf(stderr, "\tsignal: %d\n", signum);
  fprintf(stderr, "\tpid:    %d\n", pid);
  fprintf(stderr, "\tpgrp:   %d\n", pgrp);
  fprintf(stderr, "\tfrom:   %d\n", si_pid);

  // Try obtaining the process name for the sending pid.
  int ret = proc_pidpath(si_pid, si_path, sizeof(si_path));
  if (ret <= 0) {
    fprintf(stderr, "\tpath:   %s\n", strerror(errno));
  } else {
    fprintf(stderr, "\tpath:   %s\n", si_path);
  }

  exit(1);
}

static void sighandler_set(int signal) {
  struct sigaction sa;
  memset(&sa, '\0', sizeof(sa));
  sa.sa_sigaction = &sighandler_handle;
  sa.sa_flags = SA_SIGINFO;
  sigaction(signal, &sa, NULL);
}

static PyObject *sighandler_register(PyObject *self, PyObject *args) {
  int signal;
  if (!PyArg_ParseTuple(args, "i", &signal))
    return NULL;
  sighandler_set(signal);
  Py_RETURN_NONE;
}

static PyMethodDef sighandler_methods[] = {
    {"register", sighandler_register, METH_VARARGS,
     "Register signal handler by signal number"},
    {NULL, NULL, 0, NULL},
};

PyMODINIT_FUNC initsighandler(void) {
  PyObject *m = Py_InitModule("sighandler", sighandler_methods);
  if (m == NULL)
    return;
}