Native SDL2 + OpenAL bindings for Python, Ruby, and Java, powered by a shared C/C++ core.
python_sdl2 is a small cross-language multimedia binding project that exposes SDL2 windowing/rendering and OpenAL audio playback to multiple runtimes from one native implementation.
The project currently builds:
- A Python extension module:
python_sdl2.so - A Ruby native extension:
ruby_sdl2.bundle - A Java JNI library and JAR:
java_sdl2.dylib/JavaSDL2.jar
It is useful for experimenting with native language bindings, SDL2 rendering, keyboard input callbacks, simple sprite drawing, and WAV playback through OpenAL.
- Features
- Supported Languages
- Project Layout
- Requirements
- Installing Dependencies
- Building from Source
- Build Outputs
- Running the Examples
- API Overview
- Example Usage
- Development Notes
- Troubleshooting
- License
- Create native SDL2 windows.
- Poll SDL2 events.
- Register keyboard callbacks.
- Clear windows with RGBA colors.
- Draw BMP sprites.
- Draw sprites with scaling and optional bounds visualization.
- Load and play WAV files through OpenAL.
- Query SDL ticks.
- Resolve SDL keycodes from human-readable key names.
- Use a shared C/C++ native core from Python, Ruby, and Java.
- Generate Python reflection glue during the CMake build.
- Build Java JNI bindings and package Java classes into a JAR.
| Language | Binding Type | Main Import / Entry Point |
|---|---|---|
| Python | CPython native extension | import python_sdl2 |
| Ruby | Native extension bundle | require 'ruby_sdl2' |
| Java | JNI shared library + JAR | dev.linkcoder100788.java_sdl2.JavaSDL2 |
text
.
├── CMakeLists.txt
├── README.md
├── lib/
│ └── ruby_sdl2.rb
├── scripts/
│ ├── dylibChecker.sh
│ ├── leakCheck_testJava.sh
│ ├── leakCheck_testPython.sh
│ ├── leakCheck_testRuby.sh
│ ├── testJava.sh
│ ├── testPython.sh
│ └── testRuby.sh
├── src/
│ ├── bindings/
│ │ ├── java/
│ │ ├── python/
│ │ └── ruby/
│ └── core/
│ ├── PyCore.cpp
│ ├── PyCore.h
│ ├── RbCore.cpp
│ └── RbCore.h
├── test/
│ ├── assets/
│ ├── Test.java
│ ├── test.py
│ ├── test.rb
│ ├── verbose.py
│ └── verbose.rb
└── tools/
└── reflector.cpp
Important directories:
src/core/— shared native implementation used by bindings.src/bindings/python/— Python extension entry point.src/bindings/ruby/— Ruby extension entry point.src/bindings/java/— Java classes and JNI native code.tools/reflector.cpp— reflection/generation tool used by the Python binding build.test/— example programs and test assets.scripts/— convenience scripts for running examples and leak checks.
You need:
- CMake 4.3+
- A C++14-compatible compiler
- SDL2
- OpenAL
- Python 3 with development headers
- Ruby with development headers
- Java JDK with JNI headers
- LLVM / Clang for the reflection tool used during the Python build
The project is currently most directly configured for macOS-style development, especially for Java JNI include paths and Homebrew LLVM discovery.
It is recommended to install the project using homebrew.
Step 1: Tap the repository of your choice
brew tap link-coder100788/python-sdl2
brew tap link-coder100788/ruby-sdl2
brew tap link-coder100788/java-sdl2Step 2: Install the project
brew install python-sdl2
brew install ruby-sdl2
brew install java-sdl2The homebrew repositories can be found at:
Using Homebrew:
bash
brew install cmake sdl2 openal-soft llvm python ruby
You also need a JDK installed. For example:
bash
brew install openjdk
If CMake cannot find LLVM automatically, pass the LLVM CMake directory explicitly:
bash
cmake -S . -B build -DLLVM_DIR="$(brew --prefix llvm)/lib/cmake/llvm"
If OpenAL headers are installed with the OpenAL-soft layout, enable:
bash
-DUSE_HOMEBREW_OPENAL=ON
Example:
bash
cmake -S . -B build \
-DLLVM_DIR="$(brew --prefix llvm)/lib/cmake/llvm" \
-DUSE_HOMEBREW_OPENAL=ON
On Debian/Ubuntu-like systems:
bash
sudo apt update
sudo apt install \
build-essential \
cmake \
python3-dev \
ruby-dev \
default-jdk \
libsdl2-dev \
libopenal-dev \
llvm-dev \
clang
Depending on your distribution, package names may differ.
Windows support depends on your local toolchain setup.
Recommended options:
- Install dependencies through vcpkg, MSYS2, or manually.
- Make sure CMake can find:
- Python development files
- Ruby development files
- SDL2
- OpenAL
- LLVM / Clang
- JDK / JNI headers
You may need to adjust platform-specific JNI include paths in CMakeLists.txt.
From the project root:
bash
cmake -S . -B build
cmake --build build
For macOS with Homebrew LLVM and OpenAL-soft:
bash
cmake -S . -B build \
-DLLVM_DIR="$(brew --prefix llvm)/lib/cmake/llvm" \
-DUSE_HOMEBREW_OPENAL=ON
cmake --build build
The project also supports the following CMake options:
| Option | Default | Description |
|---|---|---|
BUILD_PYTHON_MODULE |
ON |
Build the Python module. |
USE_HOMEBREW_OPENAL |
OFF |
Use OpenAL-soft/Homebrew-style header layout. |
After a successful build, outputs are created in the build directory.
Common outputs include:
text
build/
├── JavaSDL2.jar
├── java_classes/
├── libjava_sdl2.dylib
├── python_sdl2.<python-extension-suffix>
├── reflect_tool
└── ruby_sdl2.bundle
On macOS, the Python extension may look similar to:
text
python_sdl2.cpython-314-darwin.so
The exact Python suffix depends on your Python version and platform.
The repository includes test programs in test/.
The examples expect to be run from the test/ directory or with paths adjusted so assets can be found under test/assets.
From the project root, after building:
bash
cd test
PYTHONPATH=../build python3 test.py
If you are using the default CLion/CMake debug directory from this repository:
bash
cd test
PYTHONPATH=../cmake-build-debug python3 test.py
From the project root:
bash
cd test
ruby test.rb
The included Ruby test currently loads the extension from the debug build directory using require_relative.
If you build into build/, update the require path or run with an appropriate load path.
Compile the Java test with the generated JAR:
bash
cd test
javac -cp ../build/JavaSDL2.jar Test.java
Run it with the native library path set to the build directory:
bash
java \
--enable-native-access=ALL-UNNAMED \
-Djava.library.path=../build \
-cp ../build/JavaSDL2.jar:. \
Test
On macOS, SDL/Cocoa applications often need to run on the first thread:
bash
java \
-XstartOnFirstThread \
--enable-native-access=ALL-UNNAMED \
-Djava.library.path=../build \
-cp ../build/JavaSDL2.jar:. \
Test
If you are using the default debug build directory:
bash
java \
-XstartOnFirstThread \
--enable-native-access=ALL-UNNAMED \
-Djava.library.path=../cmake-build-debug \
-cp ../cmake-build-debug/JavaSDL2.jar:. \
Test
The bindings expose a similar set of concepts across languages.
| Concept | Description |
|---|---|
Window |
SDL2 window wrapper for rendering and event polling. |
Sprite |
BMP sprite/texture object that can be drawn in a window. |
OpenALPlayer |
Audio player for WAV playback through OpenAL. |
ScreenCoordinate |
Simple 2D coordinate object. |
init |
Initialize native SDL/OpenAL systems. |
quit |
Shut down native systems. |
get_ticks / getTicks |
Get SDL tick count. |
get_keycode_from_name / getKeycodeFromName |
Convert key names like "a" or "space" into SDL keycodes. |
play_sound / playSound |
Play a sound file directly. |
Module:
python
import python_sdl2
Common functions:
python_sdl2.version()python_sdl2.help()python_sdl2.init()python_sdl2.quit()python_sdl2.get_ticks()python_sdl2.get_keycode_from_name(name)python_sdl2.play_sound(path)python_sdl2._debug()
Common classes:
python_sdl2.Windowpython_sdl2.Spritepython_sdl2.OpenALPlayerpython_sdl2.ScreenCoordinate
Typical methods include:
Window.poll_events()Window.clear(r, g, b, a)Window.present()Window.draw_sprite(sprite)Window.draw_sprite(sprite, scale, show_bounds)Window.set_key_callback(keycode, callback)Sprite.set_location(x, y)Sprite.set_x(x)Sprite.set_y(y)OpenALPlayer.play_sound()OpenALPlayer.set_path(path)
Module:
ruby
require 'ruby_sdl2'
Common module methods:
RubySDL2.versionRubySDL2.helpRubySDL2.initRubySDL2.quitRubySDL2.get_ticksRubySDL2.get_keycode_from_name(name)RubySDL2.play_sound(path)RubySDL2._debug
Common classes:
RubySDL2::WindowRubySDL2::SpriteRubySDL2::OpenALPlayerRubySDL2::ScreenCoordinate
Typical methods include:
Window#poll_eventsWindow#clear(r, g, b, a)Window#presentWindow#draw_sprite(sprite)Window#draw_sprite(sprite, scale, show_bounds)Window#set_key_callback(keycode, proc)Sprite#set_location(x, y)Sprite#set_x(x)Sprite#set_y(y)OpenALPlayer#play_soundOpenALPlayer#set_path(path)
Main class:
java
import dev.linkcoder100788.java_sdl2.JavaSDL2;
Nested classes:
JavaSDL2.WindowJavaSDL2.SpriteJavaSDL2.OpenALPlayerJavaSDL2.ScreenCoordinate
Common static methods:
JavaSDL2.version()JavaSDL2.help()JavaSDL2.init()JavaSDL2.quit()JavaSDL2.getTicks()JavaSDL2.getKeycodeFromName(String name)JavaSDL2.playSound(String path)JavaSDL2._debug()
Typical instance methods include:
Window.pollEvent()Window.clear(int r, int g, int b, int a)Window.present()Window.drawSpriteScaledBounded(Sprite sprite, int scale, boolean showBounds)Window.onKey(int keycode, callback)Window.destroy()Sprite.setX(int x)Sprite.setY(int y)Sprite.destroy()OpenALPlayer.playSound()OpenALPlayer.setPath(String path)OpenALPlayer.destroy()
python
import python_sdl2
import math
import time
python_sdl2.init()
window = python_sdl2.Window("Python SDL2 Example", 800, 600)
sprite = python_sdl2.Sprite("assets/aa.bmp")
x = 400
y = 300
def move_left(key):
global x
x -= 5
def move_right(key):
global x
x += 5
def move_up(key):
global y
y -= 5
def move_down(key):
global y
y += 5
window.set_key_callback(python_sdl2.get_keycode_from_name("a"), move_left)
window.set_key_callback(python_sdl2.get_keycode_from_name("d"), move_right)
window.set_key_callback(python_sdl2.get_keycode_from_name("w"), move_up)
window.set_key_callback(python_sdl2.get_keycode_from_name("s"), move_down)
running = True
t = 0.0
while running:
running = window.poll_events()
r = int(abs(math.sin(t)) * 255)
g = int(abs(math.cos(t)) * 255)
window.clear(r, g, 150, 255)
sprite.set_location(x, y)
window.draw_sprite(sprite, 5, True)
window.present()
t += (math.pi / 4.0) / 60.0
time.sleep(0.016)
python_sdl2.quit()
ruby
require 'ruby_sdl2'
RubySDL2.init
window = RubySDL2::Window.new("Ruby SDL2 Example", 800, 600)
sprite = RubySDL2::Sprite.new("assets/aa.bmp")
state = {
x: 400,
y: 300,
running: true,
t: 0.0
}
window.set_key_callback(RubySDL2.get_keycode_from_name("a"), proc { state[:x] -= 5 })
window.set_key_callback(RubySDL2.get_keycode_from_name("d"), proc { state[:x] += 5 })
window.set_key_callback(RubySDL2.get_keycode_from_name("w"), proc { state[:y] -= 5 })
window.set_key_callback(RubySDL2.get_keycode_from_name("s"), proc { state[:y] += 5 })
while state[:running]
state[:running] = false unless window.poll_events
r = (Math.sin(state[:t]).abs * 255).to_i
g = (Math.cos(state[:t]).abs * 255).to_i
window.clear(r, g, 150, 255)
sprite.set_location(state[:x], state[:y])
window.draw_sprite(sprite, 5, true)
window.present
state[:t] += (Math::PI / 4.0) / 60.0
sleep 0.016
end
RubySDL2.quit
java
import dev.linkcoder100788.java_sdl2.JavaSDL2;
import dev.linkcoder100788.java_sdl2.JavaSDL2.Window;
import dev.linkcoder100788.java_sdl2.JavaSDL2.Sprite;
public class Test {
public static void main(String[] args) {
JavaSDL2.init();
Window window = new Window("Java SDL2 Example", 800, 600);
Sprite sprite = new Sprite("assets/aa.bmp");
final int[] x = {400};
final int[] y = {300};
window.onKey(JavaSDL2.getKeycodeFromName("a"), key -> x[0] -= 5);
window.onKey(JavaSDL2.getKeycodeFromName("d"), key -> x[0] += 5);
window.onKey(JavaSDL2.getKeycodeFromName("w"), key -> y[0] -= 5);
window.onKey(JavaSDL2.getKeycodeFromName("s"), key -> y[0] += 5);
boolean running = true;
double t = 0.0;
while (running) {
running = window.pollEvent();
int r = (int)(Math.abs(Math.sin(t)) * 255);
int g = (int)(Math.abs(Math.cos(t)) * 255);
window.clear(r, g, 150, 255);
sprite.setX(x[0]);
sprite.setY(y[0]);
window.drawSpriteScaledBounded(sprite, 5, true);
window.present();
t += (Math.PI / 4.0) / 60.0;
try {
Thread.sleep(16);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
break;
}
}
sprite.destroy();
window.destroy();
JavaSDL2.quit();
}
}
The Python binding uses a native reflection helper:
text
reflect_tool
During the CMake build, this tool reads the core Python-facing header and generates binding glue files into the build directory:
text
build/generated/
├── generated_reflect.c
└── generated_reflect.h
These generated files are then compiled into the Python extension module.
The Java binding consists of:
- Java source under
src/bindings/java/ - Native JNI implementation under
src/bindings/java/native/ - Generated JNI headers
- A shared native library
- A generated
JavaSDL2.jar
The build produces:
text
JavaSDL2.jar
libjava_sdl2.dylib
On Linux, the shared library name will usually differ, such as:
text
libjava_sdl2.so
On Windows, it may be:
text
java_sdl2.dll
The native targets are compiled with warnings treated as errors:
text
-Werror
This helps keep the C/C++ code clean, but it may expose compiler- or platform-specific warnings when building on a new system.
Pass LLVM_DIR manually:
bash
cmake -S . -B build -DLLVM_DIR="$(brew --prefix llvm)/lib/cmake/llvm"
On Linux, locate your LLVM CMake package directory and pass that path instead.
Try enabling the Homebrew/OpenAL-soft layout option:
bash
cmake -S . -B build -DUSE_HOMEBREW_OPENAL=ON
Make sure the build directory is on PYTHONPATH:
bash
PYTHONPATH=../build python3 test.py
Or, for the default debug build directory:
bash
PYTHONPATH=../cmake-build-debug python3 test.py
Also confirm that the built extension exists:
bash
ls build/python_sdl2*
Make sure Ruby can find the native extension bundle.
For example:
ruby
require_relative '../build/ruby_sdl2'
or run Ruby with an adjusted load path:
bash
ruby -I../build test.rb
Make sure java.library.path points to the directory containing the native library:
bash
-Djava.library.path=../build
Also make sure the JAR is on the classpath:
bash
-cp ../build/JavaSDL2.jar:.
On Windows, use ; instead of : as the classpath separator.
Run Java with:
bash
-XstartOnFirstThread
Example:
bash
java \
-XstartOnFirstThread \
--enable-native-access=ALL-UNNAMED \
-Djava.library.path=../build \
-cp ../build/JavaSDL2.jar:. \
Test
The examples use relative paths such as:
text
assets/aa.bmp
assets/ae.bmp
assets/dingSound.wav
assets/dieSound.wav
Run examples from the test/ directory, or adjust asset paths accordingly.
MIT License.