Search code examples
pythoncswig

Deep copy in Python SWIG wrapper of C struct


I am using SWIG to generate Python bindings for a C library. The library defines a struct with value semantics. In C++ terminology the struct would be POD - copying it with memcpy produces a semantically correct copy.

clib.h:

struct s
{
     int i;
};

I compile this into a Python module using SWIG. Details on the build process are in an "appendix" of this question.

In C code, we can verify that the assignment operator for variables of struct type have value semantics:

#include <assert.h>
#include "clib.h"

int main()
{
    struct s s1;
    s1.i = 100;

    struct s s2 = s1;
    assert(s2.i == 100);

    s2.i = 101;
    assert(s1.i == 100);
}

In the Python wrapper, as expected, we instead have reference semantics:

import clib

s1 = clib.s()
s1.i = 100

s2 = s1
assert s2.i == 100

s2.i = 101
assert s1.i == 101

If our library were written in C++ instead, and s had a copy constructor, SWIG would generate one for the Python wrapper too (reference). We could write:

s3 = clib.s(s1)

but for a C library, this wrapper is not generated:

TypeError: __init__() takes exactly 1 argument (2 given)

We might hope that SWIG generates the appropriate magic methods for copy.deepcopy:

from copy import deepcopy
s4 = deepcopy(s1)

but it does not:

TypeError: can't pickle SwigPyObject objects

I am perplexed. This seems like it should be really simple, but I can find nothing. In the "SWIG and Python" documentation, the word "copy" only appears in the previously linked note on C++ copy constructors, and in some low level details about the generated wrapper code. The closest question on Stack Overflow has a horrifically complicated answer.


Details to reproduce

Define a trivial SWIG interface file clib.i:

%module clib

%{
#include "clib.h"
%}

%include "clib.h"

Create a Python module using setup.py:

from distutils.core import setup, Extension

clib = Extension(
    "_clib",
    sources=["clib_wrap.c"],
    extra_compile_args=["-g"],
)

setup(name="clib", version="1.0", ext_modules=[clib])

Build the whole thing with a Makefile:

swig: setup.py clib_wrap.c
    python2 setup.py build_ext --inplace

clib_wrap.c: clib.i
    swig -python clib.i

Then, compile/run the Python and C test programs exactly as listed above.


Solution

  • While a C library can't have constructors/destructors, you can define them after-the-fact for a SWIG wrapper ref: swig docs 5.5.6. Note that constructors have to be written carefully:

    There is one subtle difference to a normal C++ constructor implementation though and that is although the constructor declaration is as per a normal C++ constructor, the newly constructed object must be returned as if the constructor declaration had a return value.

    test.i:

    %module test
    
    %{
    #include <stdlib.h>
    #include "clib.h"
    %}
    
    %include "clib.h"
    
    %extend s {       // add additional methods to the s struct
        s(int i) {    // constructor
            struct s* t = malloc(sizeof(struct s));
            t->i = i;
            return t;
        }
        s(struct s* o) { // copy constructor
            struct s* t = malloc(sizeof(struct s));
            t->i = o->i;
            return t;
        }
        ~s() {           // destructor
            free($self);
        }
    }
    

    Use case:

    >>> import test
    >>> s1 = test.s(5)
    >>> s1.i
    5
    >>> s2 = test.s(s1)  # copy
    >>> s2.i
    5
    >>> s2.i = 7
    >>> s1.i
    5
    >>> s2.i
    7