Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust fixes #18

Open
wants to merge 43 commits into
base: stable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
28d3fe7
feat: better Rust bindings, properly wrapped mouse
ABeltramo Jul 31, 2024
639cecc
fix: absolute mouse, remove hardcoded name
ABeltramo Jul 31, 2024
f450266
fix: absolute mouse, remove hardcoded name
ABeltramo Jul 31, 2024
f812746
feat: completed Rust mouse API + tests
ABeltramo Aug 1, 2024
d216136
feat: completed Rust keyboard API + tests
ABeltramo Aug 1, 2024
264fe97
feat: added pkgconfig configuration
ABeltramo Aug 1, 2024
da41b6b
feat: allow Rust build when using the shared library
ABeltramo Aug 1, 2024
aa3f1fd
feat: added basic Dockerfile for compiling from scratch
ABeltramo Aug 1, 2024
41314dc
feat: added xbox joypad, separated c_bindings
ABeltramo Aug 4, 2024
e3e74fe
feat: added switch joypad
ABeltramo Aug 6, 2024
ca650aa
Remove obsolete extern crate lines.
hgaiser Jan 11, 2025
388c0ef
Implement Send for inputtino types.
hgaiser Jan 11, 2025
529aef8
Fix some warnings.
hgaiser Jan 11, 2025
72a2409
Rename set_led to set_on_led in C API to match C++ API.
hgaiser Jan 11, 2025
f212f56
Add Rust bindings for PS5 controller.
hgaiser Jan 11, 2025
69e23fe
Build inputtino by default in Rust binding.
hgaiser Jan 11, 2025
eeb318c
Fix crash when error occurs.
hgaiser Jan 11, 2025
a774277
Add Joypad enum to represent any joypad.
hgaiser Jan 12, 2025
78a6928
Rename Rust bindings.
hgaiser Jan 12, 2025
2d9a599
Rename XOneJoypad to XboxOneJoypad in Rust bindings.
hgaiser Jan 12, 2025
e890aa5
Rename joypad_switch.rs to joypad_nintendo.rs.
hgaiser Jan 12, 2025
59eed9e
Correct comment in lib.rs.
hgaiser Jan 12, 2025
c257012
Make static compilation the default in Rust binding.
hgaiser Jan 12, 2025
8cc65ca
Simplify Joypad::new in Rust binding.
hgaiser Jan 12, 2025
d6e4876
fix: joypad tests using SDL
ABeltramo Jan 14, 2025
8aa977d
fix: removed extra `a` caused by the virtual keyboard
ABeltramo Jan 14, 2025
5071c6f
Use SemVer for Rust binding.
hgaiser Jan 14, 2025
2e20492
Only use static linking for Rust binding.
hgaiser Jan 14, 2025
7a89e04
Rename c_bindings to ffi.
hgaiser Jan 17, 2025
fdac019
Replace make_device! macro with a function.
hgaiser Jan 17, 2025
d7dd8ca
Replace get_nodes! macro with a function.
hgaiser Jan 17, 2025
d5f87f7
Remove Inputtino prefix from common functions.
hgaiser Jan 17, 2025
38765f1
Add missing use statement.
hgaiser Jan 22, 2025
2890983
Place FFI in its own crate.
hgaiser Jan 22, 2025
4e5d1eb
Fix linking of in Rust binding.
hgaiser Jan 22, 2025
99a4008
Add Rust workflow for CI.
hgaiser Jan 22, 2025
d14e93e
Define Joypad as a trait.
hgaiser Jan 22, 2025
a2fc59e
Add TODO about keyboard codes.
hgaiser Jan 22, 2025
d9506ce
Add keyboard and mouse examples.
hgaiser Jan 22, 2025
f4d63e0
Implement Send for Joypad trait.
hgaiser Jan 22, 2025
a80770e
Correct touchpad movements for PS5 controller.
hgaiser Jan 25, 2025
a7ca191
Expose place_finger and release_finger.
hgaiser Jan 25, 2025
090204e
Switch back to enum for Joypad type.
hgaiser Jan 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
!include
!cmake/
!docker/startup.sh
!share/

# Rust bindings
!bindings/rust/
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/inputtino/${export_file_name}"
COMPONENT libinputtino-dev
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/inputtino")
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/share/pkgconfig/libinputtino.pc.in
${CMAKE_CURRENT_BINARY_DIR}/libinputtino.pc
@ONLY
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libinputtino.pc
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig)
endif ()
endif ()

Expand Down
1 change: 1 addition & 0 deletions bindings/rust/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
18 changes: 16 additions & 2 deletions bindings/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
[package]
name = "inputtino"
version = "0.1.0"
version = "2024.8.1"
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
edition = "2021"
license = "MIT"
rust-version = "1.72"
links = "libinputtino"
homepage = "https://github.com/games-on-whales/inputtino"
authors = ["ABeltramo"]
description = "Rust bindings for inputtino"

[lib]
name = "inputtino_rs"
name = "inputtino"
path = "src/lib.rs"

[build-dependencies]
bindgen = "0.69.4"
cmake = "0.1"
pkg-config = "0.3.30"

[dev-dependencies]
input = "0.9.0"
rustix = { version = "0.38.18", features = ["fs"] }
approx = "0.5.1"
sdl2 = "0.37.0"
serial_test = "3.1.1"

[dependencies]
strum_macros = "0.26.4"
81 changes: 39 additions & 42 deletions bindings/rust/build.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,61 @@
extern crate bindgen;

use std::env;
use std::path::PathBuf;

use cmake::Config;

fn main() {
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
let build_static = false;

// This is the directory where the `c` library is located.
let libdir_path = PathBuf::from("../../")
// Canonicalize the path as `rustc-link-search` requires an absolute
// path.
.canonicalize()
.expect("cannot canonicalize path");

// Compile the library using CMake
let dst = Config::new(libdir_path)
.target("libinputtino")
.define("BUILD_SHARED_LIBS", if build_static { "OFF" } else { "ON" })
.define("LIBINPUTTINO_INSTALL", "ON")
.define("BUILD_TESTING", "OFF")
.define("BUILD_SERVER", "OFF")
.define("BUILD_C_BINDINGS", "ON")
.profile("Release")
.define("CMAKE_CONFIGURATION_TYPES", "Release")
.build();

// Dependencies
if !build_static {
println!("cargo:rustc-link-lib=evdev");
println!("cargo:rustc-link-lib=stdc++");
}

//libinputtino
println!("cargo:rustc-link-search=native={}/lib", dst.display());
println!("cargo:rustc-link-lib={}libinputtino", if build_static { "static=" } else { "" });
// Options
let build_c_bindings = env::var("INPUTTINO_BUILD_C_BINDINGS").unwrap_or("TRUE".to_string()) == "TRUE";
let build_static = env::var("INPUTTINO_BUILD_STATIC").unwrap_or("TRUE".to_string()) == "TRUE";
hgaiser marked this conversation as resolved.
Show resolved Hide resolved

// The bindgen::Builder is the main entry point
// to bindgen, and lets you build up options for
// the resulting bindings.
let bindings = bindgen::Builder::default()
let mut bindings = bindgen::Builder::default()
.use_core()
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: false,
})
// Add the include directory
.clang_arg(format!("-I{}/include/", dst.display()))
// Set the INPUTTINO_STATIC_DEFINE macro
.clang_arg(if build_static {"-D INPUTTINO_STATIC_DEFINE=1"} else {""})
.clang_arg(if build_static { "-D INPUTTINO_STATIC_DEFINE=1" } else { "" })
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
// The input header we would like to generate bindings for.
.header("wrapper.hpp")
// Finish the builder and generate the bindings.
.generate()
// Unwrap the Result and panic on failure.
.expect("Unable to generate bindings");
.header("wrapper.hpp");

if build_c_bindings {
let libdir_path = PathBuf::from("../../")
// Canonicalize the path as `rustc-link-search` requires an absolute
// path.
.canonicalize()
.expect("cannot canonicalize path");

// Compile the library using CMake
let dst = Config::new(libdir_path)
.define("BUILD_SHARED_LIBS", if build_static { "OFF" } else { "ON" })
.define("LIBINPUTTINO_INSTALL", "ON")
.define("BUILD_TESTING", "OFF")
.define("BUILD_SERVER", "OFF")
.define("BUILD_C_BINDINGS", "ON")
.profile("Release")
.define("CMAKE_CONFIGURATION_TYPES", "Release")
.build();

println!("cargo:rustc-link-search=native={}/lib", dst.display());
bindings = bindings.clang_arg(format!("-I{}/include/", dst.display()))
} else {
let lib = pkg_config::probe_library("libinputtino").unwrap();
bindings = bindings.clang_arg(format!("-I{}", lib.include_paths[0].display()));
}

// Dependencies
println!("cargo:rustc-link-lib=evdev");
println!("cargo:rustc-link-lib=stdc++");
println!("cargo:rustc-link-lib={}libinputtino", if build_static { "static=" } else { "" });
hgaiser marked this conversation as resolved.
Show resolved Hide resolved

let out = bindings.generate().expect("Unable to generate bindings");

// Write the bindings to the $OUT_DIR/bindings.rs file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs");
bindings
out
.write_to_file(out_path)
.expect("Couldn't write bindings!");
}
5 changes: 5 additions & 0 deletions bindings/rust/src/c_bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![allow(non_upper_case_globals)]
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
76 changes: 76 additions & 0 deletions bindings/rust/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::ffi::CString;
use crate::c_bindings;

#[allow(dead_code)]
pub struct DeviceDefinition {
pub def: c_bindings::InputtinoDeviceDefinition,
// Keep those around since we are passing them as pointers
name: CString,
phys: CString,
uniq: CString,
}

impl DeviceDefinition {
pub fn new(name: &str, vendor_id: u16, product_id: u16, version: u16, phys: &str, uniq: &str) -> Self {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public functions and types should have docstrings.

Also, you can use impl AsRef<str> and impl Into<u16> to give callers more flexibility. For example, that would allow you to use the evdev enums without converting to a raw u16 first.

let name = CString::new(name).unwrap();
let phys = CString::new(phys).unwrap();
let uniq = CString::new(uniq).unwrap();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of unwrapping, you should return Result<Self, Something>.

let def = c_bindings::InputtinoDeviceDefinition {
name: name.as_ptr(),
vendor_id: vendor_id,
product_id: product_id,
version: version,
device_phys: phys.as_ptr(), // TODO: optional, if not present random MAC address
device_uniq: uniq.as_ptr(),
};
DeviceDefinition { def, name, phys, uniq }
}
}

pub unsafe extern "C" fn error_handler_fn(error_message: *const ::core::ffi::c_char,
user_data: *mut ::core::ffi::c_void) {
let error_str = std::ffi::CStr::from_ptr(error_message);
let user_data = user_data as *mut CString;
*user_data = CString::from(error_str);
}


#[macro_export]
macro_rules! get_nodes {
( $fn_call:expr,$var:expr ) => {
{
let mut nodes_count: core::ffi::c_int = 0;
let nodes = $fn_call($var, &mut nodes_count);
if nodes.is_null() {
return Err("Failed to get nodes".to_string());
}

let mut result = Vec::new();
for i in 0..nodes_count {
let node = std::ffi::CString::from_raw(*nodes.offset(i as isize));
result.push(node.to_str().unwrap().to_string());
}
Ok(result)
}
};
}

#[macro_export]
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
macro_rules! make_device {
($fn_call:expr, $device:expr) => {
hgaiser marked this conversation as resolved.
Show resolved Hide resolved
{
let mut error_str = std::ffi::CString::new("").unwrap();
let error_handler = crate::c_bindings::InputtinoErrorHandler {
eh: Some(error_handler_fn),
user_data: &mut error_str as *mut _ as *mut std::ffi::c_void,
};
let device = $fn_call(&$device.def, &error_handler);
if device.is_null() { // TODO: test this
let error_msg = (&mut error_str as *mut std::ffi::CString).as_ref().unwrap().to_str().unwrap();
Err(format!("Failed to create input device: {error_msg}"))
} else {
Ok(device)
}
}
};
}
67 changes: 67 additions & 0 deletions bindings/rust/src/joypad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use strum_macros::FromRepr;

use crate::{
DeviceDefinition, JoypadStickPosition, PS5Joypad, SwitchJoypad, XboxOneJoypad
};

#[derive(Debug, FromRepr)]
#[repr(u8)]
pub enum JoypadKind {
Unknown = 0x00,
Xbox = 0x01,
PlayStation = 0x02,
Nintendo = 0x03,
}

#[repr(u8)]
pub enum Joypad {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repr(u8) makes no sense here, and I'm surprised it even compiles.

I think a public trait would be more idiomatic than an enum.

Copy link
Author

@hgaiser hgaiser Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah haha it doesn't make sense indeed. It is a leftover from me trying to combine JoypadKind and Joypad in one, but ultimately decided not to.

I was considering a trait vs enum, and chose an enum currently. The reason is that you likely want to store a list of any type of joypad. With a trait you would need to store a Vec of Box with a bunch of traits. On top of that, you might want to do some specific logic if a joypad is a PS5 controller (like LED settings), for example. With an enum this becomes a simple match statement.

I'm curious what your thoughts are on this, as I'm not 100% sure my reasoning is correct :).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on the trait vs enum consideration?

Copy link

@colinmarc colinmarc Jan 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the radio silence. I think it comes down to how much unique behavior each of the gamepad types has.

If each gamepad struct has unique functionality, but share some common behavior, that should be a trait. If all the gamepad types have the same methods, then I would say there should be a pub struct Gamepad with a vendor method or field, returning the Gamepad enum, rather than a separate struct XoneGamepad, etc. (From a quick skim, this seems to be the case.)

I wouldn't really export trait enum like this - it just feels unidiomatic. The stdlib very rarely uses enums for API-central receiver objects like this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's only the DualSense controller that is unlike the others. This could be a separate impl for DualSense.

I do like the trait implementation the more I think of it. At first I wasn't sure how to handle DualSense specific features if you are dealing with a Box<dyn Joypad>, but Joypad can implement something like:

fn as_dual_sense_mut(&mut self) -> Option<&mut DualSense> {
    None // Default to `None` for non-DualSense gamepads
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work as I had hoped, because on_rumble_fn takes a on_rumble_fn: impl FnMut(i32, i32), which means the trait isn't eligible for dynamic dispatching. The rust compiler suggests to use an enum xD

inputtino/bindings/rust/inputtino/src/joypad.rs:12:8
    |
12  |     fn set_on_rumble(&mut self, on_rumble_fn: impl FnMut(i32, i32) + 'static);
    |        ^^^^^^^^^^^^^ the trait cannot be made into an object because method `set_on_rumble` has generic type parameters
    = help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `Joypad` for this new enum and using it instead:
              inputtino::PS5Joypad
              inputtino::SwitchJoypad
              inputtino::XboxOneJoypad

Any recommendations?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, right.

I understand your point about matching the C++ API, but I still think a single Joypad struct is the best option. Future Joypads could also support LEDs, so I don't see a problem with having that method be a noop if the Joypad has none.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, the callbacks are not very rusty. It would be better IMO to expose a pollable handle and not invert control. But that may be departing quite a bit from the C++ interface.

XOne(XboxOneJoypad),
PS5(PS5Joypad),
Switch(SwitchJoypad),
}

impl Joypad {
pub fn new(kind: &JoypadKind, device: &DeviceDefinition) -> Result<Joypad, String> {
match kind {
JoypadKind::Unknown | JoypadKind::Xbox => XboxOneJoypad::new(device).map(Joypad::XOne),
JoypadKind::PlayStation => PS5Joypad::new(device).map(Joypad::PS5),
JoypadKind::Nintendo => SwitchJoypad::new(device).map(Joypad::Switch),
}
}

pub fn new_xone(device: &DeviceDefinition) -> Result<Joypad, String> {
Joypad::new(&JoypadKind::Xbox, device)
}

pub fn new_ps5(device: &DeviceDefinition) -> Result<Joypad, String> {
Joypad::new(&JoypadKind::PlayStation, device)
}

pub fn new_switch(device: &DeviceDefinition) -> Result<Joypad, String> {
Joypad::new(&JoypadKind::Xbox, device)
}

pub fn set_pressed(&self, buttons: i32) {
match self {
Joypad::XOne(joypad) => joypad.set_pressed(buttons),
Joypad::PS5(joypad) => joypad.set_pressed(buttons),
Joypad::Switch(joypad) => joypad.set_pressed(buttons),
}
}

pub fn set_triggers(&self, left_trigger: i16, right_trigger: i16) {
match self {
Joypad::XOne(joypad) => joypad.set_triggers(left_trigger, right_trigger),
Joypad::PS5(joypad) => joypad.set_triggers(left_trigger, right_trigger),
Joypad::Switch(joypad) => joypad.set_triggers(left_trigger, right_trigger),
}
}

pub fn set_stick(&self, stick_type: JoypadStickPosition, x: i16, y: i16) {
match self {
Joypad::XOne(joypad) => joypad.set_stick(stick_type, x, y),
Joypad::PS5(joypad) => joypad.set_stick(stick_type, x, y),
Joypad::Switch(joypad) => joypad.set_stick(stick_type, x, y),
}
}
}
79 changes: 79 additions & 0 deletions bindings/rust/src/joypad_nintendo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::ffi::{c_int, c_void};
use crate::c_bindings::{
inputtino_joypad_switch_create,
inputtino_joypad_switch_destroy,
inputtino_joypad_switch_get_nodes,
inputtino_joypad_switch_set_on_rumble,
inputtino_joypad_switch_set_pressed_buttons,
inputtino_joypad_switch_set_stick,
inputtino_joypad_switch_set_triggers,
};
use crate::common::{DeviceDefinition, error_handler_fn};
use crate::{get_nodes, make_device, JoypadStickPosition};

pub struct SwitchJoypad {
joypad: *mut crate::c_bindings::InputtinoSwitchJoypad,
on_rumble_fn: Box<dyn FnMut(i32, i32) -> ()>,
}

impl SwitchJoypad {
pub fn new(device: &DeviceDefinition) -> Result<Self, String> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a minimum, the error type should be pub struct InputtinoError(String). Better would be an enum using thiserror or similar.

unsafe {
let dev = make_device!(inputtino_joypad_switch_create, device);
match dev {
Ok(joypad) => {
Ok(SwitchJoypad { joypad, on_rumble_fn: Box::new(|_, _| {}) })
}
Err(e) => Err(e),
}
}
}

pub fn get_nodes(&self) -> Result<Vec<String>, String> {
unsafe {
get_nodes!(inputtino_joypad_switch_get_nodes, self.joypad)
}
}

pub fn set_pressed(&self, buttons: i32) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that each of these types has the exact same methods, but that is not enforced by the compiler, indicates that they should just be one type, imo - either a trait with many impls or just one Joypad type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a trait with many impls is closer to the C API. Without changing the C/C++ API too, I think that makes more sense.

unsafe {
inputtino_joypad_switch_set_pressed_buttons(self.joypad, buttons);
}
}

pub fn set_triggers(&self, left_trigger: i16, right_trigger: i16) {
unsafe {
inputtino_joypad_switch_set_triggers(self.joypad, left_trigger, right_trigger);
}
}

pub fn set_stick(&self, stick_type: JoypadStickPosition, x: i16, y: i16) {
unsafe {
inputtino_joypad_switch_set_stick(self.joypad, stick_type, x, y);
}
}

pub fn set_on_rumble(&mut self, on_rumble_fn: impl FnMut(i32, i32) -> () + 'static) {
self.on_rumble_fn = Box::new(on_rumble_fn);
unsafe {
let state_ptr = self as *const _ as *mut c_void;
inputtino_joypad_switch_set_on_rumble(self.joypad, Some(on_rumble_c_fn), state_ptr);
}
}
}

impl Drop for SwitchJoypad {
fn drop(&mut self) {
unsafe {
inputtino_joypad_switch_destroy(self.joypad);
}
}
}

#[allow(dead_code)]
pub unsafe extern "C" fn on_rumble_c_fn(left_motor: c_int, right_motor: c_int, user_data: *mut ::core::ffi::c_void) {
let joypad: &mut SwitchJoypad = &mut *(user_data as *mut SwitchJoypad);
((*joypad).on_rumble_fn)(left_motor, right_motor);
}

unsafe impl Send for SwitchJoypad { }
Loading