SWIG Converting Python list to a char ** example fails

I am following the SWIG tutorial and I’m currently on the section: “32.9.1 Converting Python list to a char **“. The example in question returns a malloc error on my machine:

import example  
example.print_args(["a","bc","dc"])

python(57911,0x10bd32e00) malloc: *** error for object 0x7f7ee0406b90: pointer being freed was not allocated python(57911,0x10bd32e00) malloc: *** set a breakpoint in malloc_error_break to debug 1 57911 abort python 1 57911 abort python

The error is unexpected as this is exactly the code that the tutorial offers. Any help welcome! Thanks in advance

Specs:

  • MacOS Big Sur
  • Python 3.8
  • C++17

Here are my setup.py (the whole archive for reproducibility):

#!/usr/bin/env python

"""
setup.py file for SWIG example
"""

from distutils.core import setup, Extension
import os
import sys
import glob

# gather up all the source files
srcFiles = ['example.i']
includeDirs = []

srcDir = os.path.abspath('src')
for root, dirnames, filenames in os.walk(srcDir):
    for dirname in dirnames:
        absPath = os.path.join(root, dirname)
        globStr = "%s/*.c*" % absPath
        files = glob.glob(globStr)
        includeDirs.append(absPath)
        srcFiles += files

extra_args = ['-stdlib=libc++', '-mmacosx-version-min=10.7', '-std=c++17', '-fno-rtti']
os.environ["CC"] = 'clang++'
#
example_module = Extension('_example',
                           srcFiles,  # + ['example.cpp'], # ['example_wrap.cxx', 'example.cpp'],
                           include_dirs=includeDirs,
                           swig_opts=['-c++'],
                           extra_compile_args=extra_args,
                           )

setup(name='example',
      version='0.1',
      author="SWIG Docs",
      description="""Simple swig example from docs""",
      ext_modules=[example_module],
      py_modules=["example"],
      )

Answer

The example code would work with Python 2, but has a bug as well as a syntax change for Python 3. char** must be passed byte strings, which are the default in Python 2 when using "string" syntax, but need a leading b, e.g. b"string" in Python 3.

This works:

import example  
example.print_args([b"a",b"bc",b"dc"])

The crash is due to a bug calling free twice if an incorrect parameter type is found. Make the following change to the example:

      if (PyString_Check(o)) {
        $1[i] = PyString_AsString(PyList_GetItem($input, i));
      } else {
        //free($1); // REMOVE THIS FREE
        PyErr_SetString(PyExc_TypeError, "list must contain strings");
        SWIG_fail;

SWIG_fail; ends up called the freearg typemap, which calls free a second time. With this change, you should see the following if passing incorrect arguments, such as a non-list or Unicode strings instead of byte strings:

>>> import argv
>>> argv.print_args(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:argv.py", line 66, in print_args
    return _argv.print_args(argv)
TypeError: not a list
>>> argv.print_args(['abc','def'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:argv.py", line 66, in print_args
    return _argv.print_args(argv)
TypeError: list must contain strings
>>> argv.print_args([b'abc',b'def'])
argv[0] = abc
argv[1] = def
2

Changing the error message to “list must contain byte strings” would help as well 😊