Chapter 18: Interfacing with Other Languages
C is often used as a common denominator between programming languages. Many interpreters and runtimes are written in C or provide APIs to call C code directly. In this chapter you will see how to build shared libraries callable from Python with ctypes, how to integrate C with Java through the Java Native Interface (JNI), and how to embed C modules into scripting environments for customization or performance.
Creating shared libraries callable from Python (ctypes)
Python’s ctypes module lets you call C functions from shared libraries (.so, .dylib, or .dll) without writing a Python extension module. You simply define the functions in C, compile them into a shared library, and load that library at runtime in Python.
Defining a simple C library
This example defines a few functions to be called from Python. You must export them with the correct linkage attribute on Windows, and regular extern linkage on Unix like systems.
#include <stdio.h>
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
EXPORT int add(int a, int b) {
return a + b;
}
EXPORT void hello(const char *name) {
printf("Hello, %s!\n", name);
}
Compile it as a shared library:
# Linux
cc -shared -fPIC -o libexample.so example.c
# macOS
cc -shared -fPIC -o libexample.dylib example.c
# Windows (MSVC)
cl /LD example.c
Loading and calling from Python
In Python you can load and use the compiled library easily:
import ctypes
# adjust path or name to match your system
lib = ctypes.CDLL("./libexample.so")
# declare argument and return types
lib.add.argtypes = (ctypes.c_int, ctypes.c_int)
lib.add.restype = ctypes.c_int
result = lib.add(2, 3)
print("2 + 3 =", result)
lib.hello(b"Python")
.dll in the same directory as your script or add it to the system PATH. Also ensure calling conventions match; ctypes defaults to cdecl.
Passing arrays and structures
ctypes can map Python lists and structures to C arrays and structs. The layout must match exactly.
# Python side
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_double),
("y", ctypes.c_double)]
p = Point(2.0, 3.5)
lib.process_point.argtypes = [ctypes.POINTER(Point)]
lib.process_point(ctypes.byref(p))
// C side
struct Point { double x; double y; };
EXPORT void process_point(struct Point *p) {
printf("x=%.2f y=%.2f\n", p->x, p->y);
}
sizeof() and Python’s ctypes.sizeof() to confirm alignment.
Calling C from Java (JNI)
The Java Native Interface (JNI) is Java’s bridge to native code. It lets Java call C or C++ functions and lets native code call back into the JVM. JNI requires generating a header from a Java class, implementing the C functions, and loading the resulting shared library.
Declaring native methods in Java
Start with a Java class that declares native methods and loads the library:
public class HelloJNI {
static {
System.loadLibrary("hello");
}
private native void sayHello(String name);
public static void main(String[] args) {
new HelloJNI().sayHello("Java");
}
}
Compile and generate a header file:
javac HelloJNI.java
javah -jni HelloJNI # older JDKs
# or, on newer JDKs:
javac -h . HelloJNI.java
Implementing the native functions in C
The generated header defines function signatures matching the Java class and method names. You must include jni.h from the JDK and use the correct naming convention.
#include <jni.h>
#include <stdio.h>
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj, jstring name) {
const char *cname = (*env)->GetStringUTFChars(env, name, NULL);
printf("Hello from C, %s!\n", cname);
(*env)->ReleaseStringUTFChars(env, name, cname);
}
Compile and link against the JNI headers and libraries:
# Linux/macOS
cc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC -shared \
-o libhello.so HelloJNI.c
# Windows (MSVC)
cl /I "%JAVA_HOME%\include" /I "%JAVA_HOME%\include\win32" /LD HelloJNI.c
When you run the Java program, it loads libhello.so (or hello.dll) automatically and calls the native method.
Returning values to Java
Native functions can return primitives or strings. For example, returning a new string:
JNIEXPORT jstring JNICALL Java_HelloJNI_greet(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Greetings from C");
}
Then in Java:
public native String greet();
GetStringUTFChars() or array pointers that you acquire from the JVM to avoid leaks.
Embedding C in scripting environments
Instead of calling C from another language, sometimes you embed a scripting engine inside a C program to make it extensible. Many engines provide clean C APIs for loading scripts, registering native functions, and executing code dynamically.
Embedding Python with its C API
You can include Python inside a C application to add scripting capabilities. Link against the Python library and use its API to initialize the interpreter and call Python code.
#include <Python.h>
int main(void) {
Py_Initialize();
PyRun_SimpleString("print('Hello from embedded Python')");
Py_Finalize();
return 0;
}
Compile with the Python development headers and libraries:
cc embed.c -I/usr/include/python3.12 -lpython3.12 -o embed
Using Lua as a lightweight embedded language
Lua is designed for embedding. It has a small footprint and a simple C API. After linking with the Lua library, you can execute Lua scripts and register C functions that scripts can call back.
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
static int c_add(lua_State *L) {
double a = luaL_checknumber(L, 1);
double b = luaL_checknumber(L, 2);
lua_pushnumber(L, a + b);
return 1;
}
int main(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "c_add", c_add);
luaL_dostring(L, "print('Sum:', c_add(2, 3))");
lua_close(L);
return 0;
}
Choosing an embedding strategy
Embedding C into a scripting environment trades raw performance for flexibility. If you need high performance, keep heavy computation in C and expose only small, well defined entry points to the script. If flexibility matters most, expose more functions and allow the script to orchestrate behavior while C handles speed critical work.