<< Home

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.

💡 Keep interfaces simple. Use plain data types and avoid passing complex structures across language boundaries unless both sides share an identical layout and memory model.

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")
⚠️ On Windows you may need to place the .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);
}
💡 Keep arrays and structures tightly packed and consistent in size across languages. Use 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.

⚠️ JNI names and types are sensitive to case and underscores. Always regenerate headers after renaming classes or methods. Mismatches lead to "UnsatisfiedLinkError".

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();
💡 JNI requires care with memory management. Always release any 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;
}
⚠️ Each scripting engine has its own rules for managing memory and stack references. Always follow its lifecycle functions to prevent leaks or crashes.

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.

💡 A clean boundary between the scripting layer and the C core makes long term maintenance easier. Document each callable function and its expected parameter and return types clearly.