Python C Extensions and FFI Practice Problems & Exercises
Practice: C Extensions and FFI
← Back to lessonUse ctypes to load the C math library (libm) and call acos(-1.0) to obtain the value of pi.
Expected output
3.141592653589793
This tests that you can locate, load, and call a function from a real system shared library. Key steps: find the library name portably with Solution
find_library, load with CDLL, declare argument and return types to prevent silent mismatches, then call like a Python function.
import ctypes
import ctypes.util
def get_pi_from_libm() -> float:
"""Load libm and call acos(-1.0) to retrieve pi."""
# TODO: load libm, set up acos signature, call it
pass
Expected Output
3.141592653589793Hints
Hint 1: Use ctypes.CDLL to load libm (the C math library).
Hint 2: On Linux/macOS the name is "libm.so.6" or "libm.dylib"; use ctypes.util.find_library("m") to be portable.
Hint 3: Set argtypes and restype before calling.
Implement c_abs(value) by calling the C standard library's abs() function via ctypes.
Example
c_abs(-42) # → 42
c_abs(7) # → 7
Solution
import ctypes
import ctypes.util
def c_abs(value: int) -> int:
libc = ctypes.CDLL(ctypes.util.find_library("c"))
libc.abs.argtypes = [ctypes.c_int]
libc.abs.restype = ctypes.c_int
return libc.abs(value)
ctypes.c_int maps to int (32-bit signed). Without explicit argtypes / restype, ctypes defaults both to c_int, which silently truncates 64-bit values or misinterprets doubles.
import ctypes
import ctypes.util
def c_abs(value: int) -> int:
"""Call C's abs() via ctypes and return the result."""
pass
Expected Output
absolute value: 42Hints
Hint 1: Load libc (find_library("c")).
Hint 2: abs() takes c_int and returns c_int.
Hint 3: Always set argtypes and restype — without them ctypes assumes c_int for everything.
Implement c_strlen(s) that uses ctypes to call the C strlen() function on a Python string and return the result as a Python int.
Example
c_strlen("hello") # → 5
c_strlen("ctypes") # → 6
Solution
import ctypes
import ctypes.util
def c_strlen(s: str) -> int:
libc = ctypes.CDLL(ctypes.util.find_library("c"))
libc.strlen.argtypes = [ctypes.c_char_p]
libc.strlen.restype = ctypes.c_size_t
return libc.strlen(s.encode())
c_char_p accepts bytes objects and passes the underlying buffer pointer. Always .encode() Python strings — C expects bytes, not Unicode objects. c_size_t maps to the platform's size_t (64-bit on modern systems).
import ctypes
import ctypes.util
def c_strlen(s: str) -> int:
"""Call C's strlen() to get the byte length of a Python string."""
pass
Expected Output
length of 'hello' is 5Hints
Hint 1: C expects a null-terminated byte string: encode with b"hello" or str.encode().
Hint 2: Use ctypes.c_char_p for char* parameters.
Hint 3: strlen returns size_t — use ctypes.c_size_t as restype.
Define a ctypes.Structure for a 2-D point with double coordinates, then compute its distance from the origin in Python.
Example
p = Point(x=3.0, y=4.0)
distance_from_origin(p) # → 5.0
This mirrors how you would share a struct definition between Python and a C library that expects struct Point { double x; double y; }.Solution
_fields_ defines the memory layout. ctypes auto-generates __init__ accepting keyword arguments that match field names. The struct is memory-compatible with struct Point { double x; double y; } in C (no padding on most platforms for two consecutive doubles).
import ctypes
class Point(ctypes.Structure):
_fields_ = [
# TODO: define x and y as c_double
]
def distance_from_origin(p: Point) -> float:
"""Return Euclidean distance of point p from the origin."""
pass
Expected Output
Point(x=3.0, y=4.0), distance=5.0Hints
Hint 1: Subclass ctypes.Structure and define _fields_ as a list of (name, ctype) tuples.
Hint 2: Pass a pointer to the struct via ctypes.byref().
Hint 3: Accessing .x and .y on the instance returns Python floats automatically.
Implement sort_with_c_qsort that:
- Creates a
ctypesarray from the input list. - Defines a Python comparator function using
CFUNCTYPE. - Passes that comparator to libc's
qsort. - Returns the sorted list.
Example
sort_with_c_qsort([5, 2, 8, 1, 9, 3])
# → [1, 2, 3, 5, 8, 9]
Solution
import ctypes
import ctypes.util
def sort_with_c_qsort(values: list) -> list:
libc = ctypes.CDLL(ctypes.util.find_library("c"))
n = len(values)
arr = (ctypes.c_int * n)(*values)
CMP = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def comparator(a_ptr, b_ptr):
a = ctypes.cast(a_ptr, ctypes.POINTER(ctypes.c_int)).contents.value
b = ctypes.cast(b_ptr, ctypes.POINTER(ctypes.c_int)).contents.value
return (a > b) - (a < b)
cmp_func = CMP(comparator)
libc.qsort.argtypes = [
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_size_t,
CMP,
]
libc.qsort.restype = None
libc.qsort(arr, n, ctypes.sizeof(ctypes.c_int), cmp_func)
return list(arr)
CFUNCTYPE wraps the Python function in a C-callable trampoline. The GC must keep cmp_func alive for the duration of qsort — store it in a variable, not a temporary.
import ctypes
import ctypes.util
def sort_with_c_qsort(values: list) -> list:
"""
Sort a list of ints using C's qsort() with a Python comparator callback.
Return the sorted list.
"""
pass
Expected Output
comparator called by qsort, array sorted correctlyHints
Hint 1: Define the callback type with ctypes.CFUNCTYPE(return_type, *arg_types).
Hint 2: The qsort comparator signature is int cmp(const void*, const void*).
Hint 3: Cast the void pointers to ctypes.POINTER(ctypes.c_int) to dereference them.
Implement double_array_inplace that allocates a C integer array, walks it via a raw ctypes pointer (incrementing each element by multiplying by 2), and returns the result as a Python list.
Example
double_array_inplace([1, 2, 3, 4, 5])
# → [2, 4, 6, 8, 10]
Solution
import ctypes
def double_array_inplace(data: list) -> list:
n = len(data)
arr_type = ctypes.c_int * n
arr = arr_type(*data)
ptr = ctypes.cast(arr, ctypes.POINTER(ctypes.c_int))
for i in range(n):
ptr[i] *= 2
return list(arr)
ctypes.cast reinterprets the array buffer as a pointer. Subscript access ptr[i] performs the pointer offset arithmetic — equivalent to *(ptr + i) in C. The underlying buffer is shared, so arr reflects the changes immediately.
import ctypes
def double_array_inplace(data: list) -> list:
"""
Create a ctypes c_int array from data, double every element
via pointer arithmetic, and return the modified list.
"""
pass
Expected Output
doubled array: [2, 4, 6, 8, 10]Hints
Hint 1: Create a ctypes array type: (c_int * n)(*data).
Hint 2: Pass it to ctypes.cast(..., POINTER(c_int)) to get a raw pointer.
Hint 3: Index the pointer with [i] to read or write individual elements.
Use cffi (in API/in-line mode) to call floor() from libm.
Example
floor_via_cffi(3.7) # → 3.0
floor_via_cffi(-1.2) # → -2.0
Solution
import cffi
import ctypes.util
def floor_via_cffi(value: float) -> float:
ffi = cffi.FFI()
ffi.cdef("double floor(double x);")
lib_path = ctypes.util.find_library("m")
libm = ffi.dlopen(lib_path)
return libm.floor(value)
ffi.cdef() parses a subset of C declarations. ffi.dlopen() loads the shared library. cffi automatically marshals Python floats to C double and back, and its overhead per call is lower than ctypes for hot paths because the generated trampoline is more direct.
def floor_via_cffi(value: float) -> float:
"""Use cffi to call libm's floor() function."""
# TODO: import cffi, declare floor, call it
pass
Expected Output
floor(3.7) = 3.0Hints
Hint 1: Use cffi.FFI() and call ffi.cdef() with the C declaration.
Hint 2: Open the library with ffi.dlopen(lib_path).
Hint 3: Call the function through the returned lib object.
Write the complete C source for a CPython extension module named myext that exposes a single function add(a: int, b: int) -> int.
Your answer should include:
- The full
.csource with correctPyMethodDef,PyModuleDef, andPyMODINIT_FUNC. - A brief explanation of each section.
(You do not need to actually compile it — focus on correctness of the source.) Section breakdown:Solution
PyArg_ParseTuple: parses positional args from Python into C variables. "ll" means two C long values.PyLong_FromLong: converts a C long back to a Python int object, incrementing the reference count.PyMethodDef: maps Python name → C function pointer + calling convention + docstring.PyModuleDef + PyModule_Create: registers the module with the interpreter.PyMODINIT_FUNC PyInit_myext: the mandatory entry point — name must match the module name.
# myext.c — write the C source as a Python string,
# then compile and import it at runtime using cffi or distutils.
# For this exercise, write out the complete C source that would
# implement a module named "myext" with a single function add(a, b).
C_SOURCE = """
/* TODO: fill in the CPython extension C source */
"""
Expected Output
myext.add(3, 4) == 7Hints
Hint 1: Define a C function with signature PyObject* add(PyObject* self, PyObject* args).
Hint 2: Use PyArg_ParseTuple(args, "ll", &a, &b) to extract two C longs.
Hint 3: Return PyLong_FromLong(a + b).
Hint 4: Register in a PyMethodDef array and create a module with PyModule_Create.
Implement zero_copy_sum that accesses an array.array of doubles through the buffer protocol and sums the values via ctypes — without copying the buffer.
Example
import array
data = array.array("d", [1.1, 2.2, 3.3, 4.4])
zero_copy_sum(data) # → 11.0
Solution
import ctypes
import array
def zero_copy_sum(arr: array.array) -> float:
n = len(arr)
# from_buffer shares the memory — no copy
c_arr = (ctypes.c_double * n).from_buffer(arr)
total = 0.0
for v in c_arr:
total += v
return total
from_buffer creates a ctypes array that aliases the same memory as arr. Writing to c_arr[i] would also modify arr[i]. This is the foundation of how NumPy, PIL, and other libraries expose their internal buffers to C extensions without serialisation.
Production use: prefer sum(c_arr) or pass the pointer to a C BLAS routine for real workloads.
import ctypes
import array
def zero_copy_sum(arr: array.array) -> float:
"""
Compute the sum of a Python array.array of doubles
using ctypes, without copying the data.
Return the result as a Python float.
"""
pass
Expected Output
sum computed without copying array dataHints
Hint 1: Use memoryview(arr) to get a view of the underlying buffer.
Hint 2: Pass memoryview to ctypes via (c_double * n).from_buffer(mv).
Hint 3: from_buffer shares memory — no copy occurs.
Model the common C pattern where a function writes its result into a pointer argument (double *output). Use ctypes.byref (or ctypes.POINTER) to receive the out-parameter.
Since we cannot compile C inline here, implement using math.sqrt as the underlying engine but wrap the call in the full ctypes out-parameter dance to demonstrate the pattern.
Example
call_compute_sqrt(9.0) # → 3.0
call_compute_sqrt(2.0) # → 1.4142...
call_compute_sqrt(-1.0) # → raises ValueError
Solution
import ctypes
import math
# Simulate the C function using a Python ctypes callback
COMPUTE_SQRT_TYPE = ctypes.CFUNCTYPE(
ctypes.c_int,
ctypes.c_double,
ctypes.POINTER(ctypes.c_double),
)
def _impl(input_val, output_ptr):
if input_val < 0:
return -1
output_ptr[0] = math.sqrt(input_val)
return 0
_c_compute_sqrt = COMPUTE_SQRT_TYPE(_impl)
def call_compute_sqrt(value: float) -> float:
result = ctypes.c_double(0.0)
rc = _c_compute_sqrt(value, ctypes.byref(result))
if rc != 0:
raise ValueError(f"compute_sqrt failed for input {value}")
return result.value
ctypes.byref(result) is equivalent to &result in C — it passes the address of the c_double variable. The C function writes through the pointer with *output = ..., which here becomes output_ptr[0] = ... in the Python callback. After the call result.value holds the written value.
import ctypes
import ctypes.util
# Imagine a C function with this signature:
# int compute_sqrt(double input, double *output);
# It writes the square root into *output and returns 0 on success, -1 on error.
# We simulate it with a small inline C string compiled via cffi.
FAKE_C_SRC = """
int compute_sqrt(double input, double *output) {
if (input < 0) return -1;
*output = /* square root calculation */;
return 0;
}
"""
def call_compute_sqrt(value: float) -> float:
"""
Call the simulated C function using ctypes byref pattern.
Return the computed square root, or raise ValueError on negative input.
"""
pass
Expected Output
result written into output parameter, returned to PythonHints
Hint 1: Declare the output parameter as POINTER(c_double).
Hint 2: Pass ctypes.byref(result_var) to let C write into result_var.
Hint 3: Read back result_var.value after the call.
Design the complete cffi out-of-line build script for a dot_product(a, b, n) C function, and show how you would:
- Write the C implementation inside
set_source. - Compile it into a Python extension module.
- Call it from Python and verify against
numpy.dot.
Solution
Build script (_dot_product_build.py):
import cffi
ffi = cffi.FFI()
ffi.cdef("""
double dot_product(const double *a, const double *b, int n);
""")
ffi.set_source(
"_dot_product",
"""
double dot_product(const double *a, const double *b, int n) {
double result = 0.0;
for (int i = 0; i < n; i++) {
result += a[i] * b[i];
}
return result;
}
""",
)
if __name__ == "__main__":
ffi.compile(verbose=True)
Run once:
python _dot_product_build.py
This produces _dot_product.cpython-3XX-*.so (or .pyd on Windows).
Usage script:
import numpy as np
from _dot_product import ffi, lib
def call_dot(a_list, b_list):
n = len(a_list)
a_buf = ffi.new("double[]", a_list)
b_buf = ffi.new("double[]", b_list)
return lib.dot_product(a_buf, b_buf, n)
a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]
result = call_dot(a, b)
expected = np.dot(a, b)
print(f"cffi: {result}") # 32.0
print(f"numpy: {expected}") # 32.0
assert abs(result - expected) < 1e-10
Key differences from ctypes:
- cffi verifies the declaration against the actual C headers at compile time (ABI-safe).
ffi.new("double[]", data)allocates a C array and copies from Python — comparable to(c_double * n)(*data).- Out-of-line mode compiles once; imports are as fast as any C extension thereafter.
# Step 1: write the build script content as a Python string.
# Step 2: describe how to run it and import the result.
# Step 3: call dot_product from the compiled module.
BUILD_SCRIPT = """
# _dot_product_build.py — run this once to compile the extension
import cffi
ffi = cffi.FFI()
ffi.cdef("""
double dot_product(const double *a, const double *b, int n);
""")
ffi.set_source(
"_dot_product", # module name
"""
/* TODO: implement dot_product in C */
""",
)
if __name__ == "__main__":
ffi.compile(verbose=True)
"""
def python_dot_product(a: list, b: list) -> float:
"""Pure-Python reference implementation for verification."""
return sum(x * y for x, y in zip(a, b))
Expected Output
Python calls compiled C dot_product, result matches numpyHints
Hint 1: cffi out-of-line mode: write a build script that calls ffi.set_source() then ffi.compile().
Hint 2: In the build script: ffi.cdef() declares the public API; ffi.set_source() provides the C implementation.
Hint 3: Import the generated extension module just like any other Python module.
Hint 4: Verify the result against numpy.dot for correctness.
