import tempfile
import ctypes
from distutils.ccompiler import new_compiler
tmp_dir = tempfile.gettempdir()
c_lib_name = 'test.so'
c_code=r"""
#include <stdio.h>
int main() {
printf("Hello world!\n");
return(0);
}
"""
with open(f"{tmp_dir}/test.c", 'w') as out:
out.write(c_code)
compiler = new_compiler()
link_objects = compiler.compile([f"{tmp_dir}/test.c"], output_dir=tmp_dir, include_dirs=[tmp_dir])
compiler.link_shared_object(link_objects, c_lib_name, library_dirs=[tmp_dir], output_dir=tmp_dir)
c_lib = ctypes.CDLL(f"{tmp_dir}/{c_lib_name}")
return_value = c_lib.main()
This rather short program will import two helper libraries, In case it's not clear, this does require a compiler locally on the machine. In my case it will use cc .
The ['cc', '-I/tmp', '-c', 'test.c', '-o', '/tmp/test.o']
['cc', '-shared', '/tmp/test.o', '-L/tmp', '-o', '/tmp/test.so']
You can poke around in the cpython/Lib/distutils/ccompiler.py and see what it does.
And on line #910 is where the compiler and linker code gets executed.
Once the source code in int main() {
}
After we've imported the library, we can call The shared C library in this example is not to equal a Python C library, since we haven't called Py_INIT among other things. I would recommend avoiding to call Python specifics such as PyObject or PyFloat_FromDouble as they will behave very strangely unless you know what you're doing.
cPython is pretty fast as is, but this allows you to create anything you want that isn't already included in the cPython core.
Perhaps secure string management? Custom socket layer? Accessing low level device API's etc.
I know that numpy for sure uses it quite a bit, with some modifications.
Most commonly this method is probably used during setup, like pandas is doing to compile different extensions.
So a more useful example of what this could be used for, would be to enhance functionality.
Lets say we want to create a more accurate round((2.675 * 100)) / 100
But that's not the point of this exercise, so we'll use a C library for this:
import tempfile
import ctypes
import pathlib
from distutils.ccompiler import new_compiler
tmp_dir = tempfile.gettempdir()
c_lib_name = 'test.so'
c_code=r"""
#include <math.h>
float round_(float num, int decimals) {
return roundf(num * pow(10, decimals)) / pow(10, decimals);
}
"""
with open("{tmp_dir}/test.c", 'w') as out:
out.write(c_code)
compiler = new_compiler()
link_objects = compiler.compile([f"{tmp_dir}/test.c"],
extra_preargs=['-fPIC'],
output_dir=tmp_dir,
include_dirs=[tmp_dir])
compiler.link_shared_object(link_objects,
c_lib_name,
library_dirs=[tmp_dir],
extra_preargs=['-fPIC'],
output_dir=tmp_dir)
c_lib = ctypes.CDLL(f"{tmp_dir}/{c_lib_name}")
c_lib.round_.restype = ctypes.c_float
value = c_lib.round_(ctypes.c_float(2.675), ctypes.c_int(2))
print(value)
This lets the C code handle the number and round it to the closest matching number.
The conversion back from a import os
import ctypes
import pathlib
from distutils.ccompiler import new_compiler
class CompilationError(BaseException):
pass
class ExternalLibrary():
def __init__(self, source, libname, working_directory="/tmp"):
self.source = source
self.libname = libname
self.working_directory = working_directory
self.link_objects = []
self.is_clean = False
self.compiled = False
def compile(self):
compiler = new_compiler()
original_directory = os.getcwd()
os.chdir(self.working_directory)
with open(f'{self.libname}.c', 'w') as out:
out.write(self.source)
self.link_objects = compiler.compile([f'{self.libname}.c'],
extra_preargs=['-fPIC'],
output_dir=self.working_directory,
include_dirs=[self.working_directory])
compiler.link_shared_object(self.link_objects,
self.libname,
library_dirs=[self.working_directory],
extra_preargs=['-fPIC'],
output_dir=self.working_directory)
lib_path = pathlib.Path().absolute() / self.libname
lib = ctypes.CDLL(lib_path)
os.chdir(original_directory)
self.clean()
return lib
def clean(self):
if not self.is_clean:
# Clean up any build files, linker files etc.
if os.path.isfile(f"{self.working_directory}/{self.libname}.c"):
os.remove(f"{self.working_directory}/{self.libname}.c")
for obj in self.link_objects:
if os.path.isfile(obj):
os.remove(obj)
if os.path.isfile(f"{self.working_directory}/{self.libname}"):
os.remove(f"{self.working_directory}/{self.libname}")
self.is_clean = True
def __enter__(self):
try:
lib = self.compile()
self.compiled = True
return lib
except:
self.compiled = False
return self
def __exit__(self, *args, **kwargs):
self.clean()
if self.compiled is False:
raise CompilationError(f"Could not compile external library {self.libname}")
return True
def round_(num, decimal_places=0):
c_code=r"""
#include <math.h>
float round_(float num, int decimals) {
return roundf(num * pow(10, decimals)) / pow(10, decimals);
}
"""
with ExternalLibrary(c_code, "round.so") as c_lib:
c_lib.round_.restype = ctypes.c_float
return round(c_lib.round_(ctypes.c_float(num), ctypes.c_int(decimal_places)), decimal_places)
print(round_(2.675, 2))
2.68
It might look like a lot, but it's essentially just a