SWIG and Ctypes#

Interfacing Fortran code through C interface

I found this reference with good examples.

Problem: Collatz conjecture#

  • Choose \(u_0\)

  • If \(u_k\) even, \(u_{k+1} \rightarrow \frac{u_k}{2}\) ;

  • If \(u_k\) odd, \(u_{k+1} \rightarrow 3 u_k+1\)

  • The Collatz conjecture is: For all \(u_0>0\) , the process will eventually reach \(u_k=1\).

  • The programs below compute number of steps (named flight) to reach \(f(u_0)\) for \(1\leq u_0 \leq N\), \(N\) given.

The Collatz conjecture on Wikipedia

C program#

%%file syracuse.c
#include <stdlib.h> 
#include <stdio.h>
long syracuse(long n) { 
   long count = 0L ; 
   while (n > 1) {
      if ((n&1)==0) 
          n /= 2; 
      else 
          n = 3*n+1; 
      count++;   
   }
   return count ; 
}

int main() {
   const long N = 1000000; 
   double t1, t2;
   long i , *flights ;
   flights = (long*)malloc(N*sizeof(long));
   for (i = 0; i <N; i++) flights[i] = syracuse(i+1); 
   return EXIT_SUCCESS;
}
Writing syracuse.c
%%bash
gcc -O3 syracuse.c 
time ./a.out

real	0m0.001s
user	0m0.001s
sys	0m0.000s

Python program#

%%time

from itertools import count

def syracuse(n):
    x = n
    for steps in count() :
        if x & 1 : 
            x = 3*x+1
        else:
            x = x // 2
            
        if x == 1:
            return steps

N = 1000000
flights = [syracuse(i) for i in range(1,N+1)]
CPU times: user 9.55 s, sys: 743 µs, total: 9.55 s
Wall time: 9.55 s

Performances#

  • The python syntax is simpler.

  • 100 times slower

  • Solution : call the C function from python.

Ctypes#

This is the C function we will call from python

%%file syrac.c

long syracuse(long n)
{ 
   long count = 0L ; 
   while (n > 1)
   {
      if ((n&1)==0) 
         n /= 2; 
      else 
         n = 3*n+1; 
      count++;   
   }
   return count ; 
}
Writing syrac.c

Build the shared library

%%bash
gcc -fPIC -shared -O3 \
    -o syrac.so syrac.c
%%time

import time
from ctypes import *

syracDLL = CDLL("./syrac.so")
syracuse = syracDLL.syracuse

flights = [syracuse(i) for i in range(1,N+1)]
CPU times: user 346 ms, sys: 12 ms, total: 358 ms
Wall time: 358 ms

Ctypes with Fortran module#

If you change the fortran file you have to restart the kernel

%%file syrac.F90

module syrac_f90
  use iso_c_binding
  implicit none

contains

  function f_syrac(n) bind(c, name='c_syrac') result(f)
    
    integer(c_long) :: f
    integer(c_long), intent(in), value :: n
    integer(c_long) :: x
    x = n
    f = 0_8
    do while(x>1)
       if (iand(x,1_8) == 0) then
          x = x / 2
       else
          x = 3*x+1
       end if
       f = f + 1_8
    end do

  end function f_syrac

end module syrac_f90
Writing syrac.F90
%%bash
rm -f *.o *.so *.dylib
gfortran -fPIC -shared -O3 -o syrac.dylib syrac.F90
from ctypes import *

syrac_f90 = CDLL('./syrac.dylib')

syrac_f90.c_syrac.restype = c_long

syrac_f90.c_syrac(1000)
111
%%time
N = 1000000
flights = [syrac_f90.c_syrac(i) for i in range(1,N+1)]
CPU times: user 408 ms, sys: 4 ms, total: 412 ms
Wall time: 411 ms
  • Faster than pure Python

  • We can call function from DLL windows libraries.

  • Unfortunately you need to adapt the syntax to the operating system.

http://docs.python.org/library/ctypes.html}

SWIG#

Interface file syrac.i for C function in syrac.c

%%file syrac.i

%module syracuseC
%{
   extern long syracuse(long n);
%}
extern long syracuse(long n);
Writing syrac.i
%%bash
swig -python syrac.i

Build the python module#

  • Using command line

swig -python syrac.i

gcc `python3-config --cflags` -fPIC \
  -shared -O3 -o _syracuseC.so syrac_wrap.c syrac.c `python3-config --ldflags`
  • With distutils

%%file setup.py
from numpy.distutils.core import Extension, setup


module_swig = Extension('_syracuseC', sources=['syrac_wrap.c', 'syrac.c'])

setup( name='Syracuse',
       version = '0.1.0',
       author      = "Pierre Navaro",
       description = """Simple C Fortran interface example """,
       ext_modules = [module_swig],
)
Writing setup.py
import sys, os

if sys.platform == "darwin":
    os.environ["CC"] = "gcc-10"
    
!{sys.executable} setup.py build_ext --inplace --quiet
/home/runner/work/python-fortran/python-fortran/notebooks/setup.py:1: DeprecationWarning: 

  `numpy.distutils` is deprecated since NumPy 1.23.0, as a result
  of the deprecation of `distutils` itself. It will be removed for
  Python >= 3.12. For older Python versions it will remain present.
  It is recommended to use `setuptools < 60.0` for those Python versions.
  For more details, see:
    https://numpy.org/devdocs/reference/distutils_status_migration.html 


  from numpy.distutils.core import Extension, setup
Traceback (most recent call last):
  File "/home/runner/work/python-fortran/python-fortran/notebooks/setup.py", line 1, in <module>
    from numpy.distutils.core import Extension, setup
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/core.py", line 24, in <module>
    from numpy.distutils.command import config, config_compiler, \
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/command/config.py", line 19, in <module>
    from numpy.distutils.mingw32ccompiler import generate_manifest
  File "/usr/share/miniconda/envs/python-fortran/lib/python3.9/site-packages/numpy/distutils/mingw32ccompiler.py", line 27, in <module>
    from distutils.msvccompiler import get_build_version as get_build_msvc_version
ModuleNotFoundError: No module named 'distutils.msvccompiler'
import _syracuseC

syracuse = _syracuseC.syracuse
syracuse(1000)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[15], line 1
----> 1 import _syracuseC
      3 syracuse = _syracuseC.syracuse
      4 syracuse(1000)

ModuleNotFoundError: No module named '_syracuseC'
%%time
N=1000000

flights = [syracuse(i) for i in range(1,N+1)]
CPU times: user 260 ms, sys: 4 ms, total: 264 ms
Wall time: 263 ms

References#