joystick_web_logo

Joystick C++ library

v2.0.7

Table of contents

Overview

Joystick is a C++ library which assists developers in interfacing with various types of joysticks, all without relying on third-party dependencies. The library is compatible with Linux (uses Linux native joysticks interface) and Windows (uses WinAPI). The library is a CMake project and utilizes C++17 standard.

Joystick is general-purpose software: it is not tied to any sector or application, and the developer of a program decides what the program does with it.

Versions

Table 1 - Library versions.

Version Release date What’s new
1.0.0 27.11.2022 - First version
2.0.0 04.10.2023 - Interface changed.
- Class name changed from JoystickController to Joystick.
- SDL2 library excluded. No more third-party dependencies.
- Documentation updated.
2.0.1 04.10.2023 - Method description updated.
2.0.2 15.10.2023 - Documentation updated.
- Files structure changed.
- Variables names changed.
2.0.3 15.04.2024 - Documentation updated.
2.0.4 17.05.2024 - Documentation updated.
2.0.5 18.07.2024 - CMake structure updated.
2.0.6 22.05.2026 - Fixed race conditions.
2.0.7 15.10.2026 - Linux: closeJoystick(), openJoystick() and the destructor no longer wait for a joystick event.
- Linux: hat value is read under the mutex, a joystick with less than two axes has no hat (out-of-range read fixed).
- Linux: getButtonState() and getAxisValue() return FALSE / -1 for an id out of range instead of throwing std::out_of_range.
- Linux: the directory /dev/input is closed after the search, joysticks with non-consecutive numbers are found, the joystick name is always terminated.
- Linux: the initial state of the joystick (JS_EVENT_INIT events) is applied, a disconnected joystick is closed by the class, the state of a closed joystick is cleared, descriptors are opened with O_CLOEXEC and O_NONBLOCK, a failed openJoystick() leaves no open descriptor.
- Windows: buttons 4 - 31 of a usual joystick are read, an uninitialized read and a shift out of range in getButtonState() fixed, openJoystick() rejects ids out of range.
- All methods are thread-safe.
- Memory and thread errors are handled: findJoysticks() returns an empty vector, openJoystick() returns FALSE.
- Compiler warnings fixed, hardening compiler and linker flags added, standard C++ without compiler extensions.
- Test application runs automated tests (interactive test: –interactive), example is limited in time and checks the results of calls.
- Documentation updated.

Library files

The library is supplied only by source code. The user is given a set of files in the form of a CMake project (repository). The repository structure is shown below:

CMakeLists.txt ------------ Main CMake file of the library.
src ----------------------- Folder with library source code.
    CMakeLists.txt -------- CMake file of the library.
    Joystick.h ------------ Main library header file.
    JoystickVersion.h ----- Header file with library version.
    JoystickVersion.h.in -- CMake service file to generate version header.
    Joystick.cpp ---------- C++ implementation file.
test ---------------------- Folder for test application files.
    CMakeLists.txt -------- CMake file for test application.
    main.cpp -------------- Source C++ file of test application.
example ------------------- Folder for example application files.
    CMakeLists.txt -------- CMake file for example application.
    main.cpp -------------- Source C++ file of example application.
static --------------------- Folder with images of the documentation.
    joystick_web_logo.png - Logo image.

Joystick class description

Joystick class declaration

Joystick interface class declared in Joystick.h file. All methods of the class are thread-safe: they can be called from different threads at the same time, but an object must not be destroyed while another thread calls its methods. The class cannot be copied or moved. Class declaration:

namespace rapidpixel
{
/// Joystick class.
class Joystick
{
public:

    /// Get Joystick class version.
    static std::string getVersion();

    /// Class constructor.
    Joystick() = default;

    /// Class destructor. Closes the open joystick.
    ~Joystick();

    /// Copy and move operations are deleted.
    Joystick(const Joystick&)            = delete;
    Joystick& operator=(const Joystick&) = delete;
    Joystick(Joystick&&)                 = delete;
    Joystick& operator=(Joystick&&)      = delete;

    /// Find available joysticks.
    std::vector<JoystickInfo> findJoysticks();

    /// Open joystick.
    bool openJoystick(int id);

    /// Close joystick.
    void closeJoystick();

    /// Check status of a button.
    bool getButtonState(int buttonId) const;

    /// Get hat value.
    int getHatValue() const;

    /// Get axis value.
    int getAxisValue(int axisId) const;
};
}

getVersion method

The getVersion() method returns string of current class version. Method declaration:

static std::string getVersion();

Method can be used without Joystick class instance:

std::cout << "Joystick class version: " << rapidpixel::Joystick::getVersion();

Console output:

Joystick class version: 2.0.7

findJoysticks method

The findJoysticks() method detects connected joysticks and provides information about joysticks. When called, this method returns a vector of JoystickInfo structures, each containing properties of the detected joysticks. Method declaration:

std::vector<JoystickInfo> findJoysticks();

Returns: vector of JoystickInfo structures. The vector is empty if there are no joysticks or if the search failed. JoystickInfo structure defined in Joystick.h file. Structure declaration:

/// Information about joystick.
struct JoystickInfo
{
    /// Joystick name.
    std::string name{ "" };
    /// Joystick id. The value is passed to Joystick::openJoystick(...).
    int id{ 0 };
    /// Number of buttons.
    int numButtons{ 0 };
    /// Number of axes.
    int numAxes{ 0 };
};

Table 2 - Joystick properties description.

Parameter Description
name Name of joystick.
id Joystick id for the openJoystick(…) method. On Linux the id is the number in the name of the device file /dev/input/js<id>, on Windows it is the number of the joystick in the multimedia joystick API. The ids of the found joysticks are not always consecutive.
numButtons Number of available buttons.
numAxes Number of supported axes.

openJoystick method

The openJoystick(…) method opens joystick which is specified by id. Only one joystick can be open: the joystick which is open at the moment of the call is closed first, also if the requested joystick cannot be opened. On Windows the method does not check that the joystick is connected. Method declaration:

bool openJoystick(int id);
Parameter Value
id Joystick id from the JoystickInfo structure returned by the findJoysticks() method. Must not be negative.

On Linux a joystick which is disconnected while it is open is closed by the class: the methods behave as if no joystick is open until the openJoystick(…) method is called again.

Returns: TRUE if the joystick is open or FALSE if not (the id is out of range or the joystick cannot be opened). If FALSE is returned no joystick is open.

closeJoystick method

The closeJoystick() method closes an opened joystick. The method can be called if no joystick is open. On Linux the method waits until the event thread of the class stops (not longer than 0.1 second plus the time of processing of one event). Method declaration:

void closeJoystick();

getButtonState method

The getButtonState(…) method serves the purpose of reading state of a button from a joystick. Method declaration:

bool getButtonState(int buttonId) const;
Parameter Value
buttonId Button id from 0 to [numButtons - 1], where numButtons is the value of the JoystickInfo structure.

Returns: TRUE if button is pressed or FALSE if not. FALSE is also returned if no joystick is open, if buttonId is out of range or if the state cannot be read.

getHatValue method

The getHatValue() method is designated to read hat value from a joystick. Method declaration:

int getHatValue() const;

On Linux the last two axes of the joystick are the hat axes (the horizontal axis, then the vertical axis). A joystick with less than two axes has no hat.

Returns: hat value from 0 to 8. If no joystick is open the method returns -1. If the state cannot be read the method returns 0. Return values:

Value hat position
0 Not pressed
1 Up
2 Up and right
3 Right
4 Right and down
5 Down
6 Down and left
7 Left
8 Left and up

getAxisValue method

The getAxisValue() method is designated to read axis value from a joystick. On Linux the ids 2, 3 and 4 select the driver axes 3, 4 and 2 (the ids follow the XInput order of a gamepad), the other ids select the driver axis with the same number. Method declaration:

int getAxisValue(int axisId) const;
Parameter Value
axisId Axis id from 0 to [numAxes - 1], where numAxes is the value of the JoystickInfo structure.

Returns: Axis position as the operating system reports it. The range depends on the platform and on the joystick: -32767 to 32767 on Linux, usually 0 to 65535 for a usual joystick on Windows, -32768 to 32767 for the sticks and 0 to 255 for the triggers of an XInput gamepad. If no joystick is open, if axisId is out of range or if the state cannot be read the method returns -1 (on Linux -1 is also a possible axis position).

Build and connect to your project

Build stand alone

The library does not depend on other repositories. Typical commands to build the library, the test application and the example (use the URL of the Joystick repository provided to you):

git clone <Joystick repository URL>
cd Joystick
mkdir build
cd build
cmake ..
make

On Windows use cmake --build . --config Release instead of make. If Joystick is the top-level CMake project, the test application and the example are built.

CMake options

Table 3 - CMake options of the repository. In the options below ${PARENT} is the prefix defined by the top-level project (empty if Joystick is the top-level project).

Option Default Description
${PARENT}_JOYSTICK ON Build the library.
${PARENT}_JOYSTICK_TEST ON (stand alone), OFF (included in other project) Build the test application.
${PARENT}_JOYSTICK_EXAMPLE ON (stand alone), OFF (included in other project) Build the example.
${PARENT}_SUBMODULE_CACHE_OVERWRITE ON Forced rewriting of the options above in the CMake cache. The top-level project sets it to OFF when it includes Joystick as a sub-repository.
CMAKE_BUILD_TYPE Release (if not specified) Build type.

Connect to your project

If you want to connect the Joystick library to your CMake project as source code, make the following steps. The library does not depend on other repositories: only the Joystick repository must be added. For example, if your repository has structure:

CMakeLists.txt
src
    CMakeList.txt
    yourLib.h
    yourLib.cpp

Create 3rdparty folder in your repository. Copy Joystick repository to 3rdparty folder. The structure of your repository will be as follows:

CMakeLists.txt
src
    CMakeList.txt
    yourLib.h
    yourLib.cpp
3rdparty
    Joystick

Create CMakeLists.txt file in 3rdparty folder. CMakeLists.txt should contain:

cmake_minimum_required(VERSION 3.13)

################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)

################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)

################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
add_subdirectory(Joystick)

File 3rdparty/CMakeLists.txt adds folder Joystick to your project and excludes test application and examples from compiling (by default test applications and example excluded from compiling if Joystick included as sub-repository). The structure of your repository:

CMakeLists.txt
src
    CMakeList.txt
    yourLib.h
    yourLib.cpp
3rdparty
    CMakeLists.txt
    Joystick

Next you need include folder 3rdparty in main CMakeLists.txt file of your repository. Add string at the end of your main CMakeLists.txt:

add_subdirectory(3rdparty)

Next you have to include Joystick library in your src/CMakeLists.txt file:

target_link_libraries(${PROJECT_NAME} Joystick)

Done!

Portability

The library is supported on Linux and Windows. It does not compile on other operating systems.

  • Linux: the library uses the kernel joystick interface (device files /dev/input/js<id>). The user needs the permission to read the device files. While a joystick is open the library runs one thread which reads the events of the joystick. The library is linked with the thread library of the system.
  • Windows: the library uses the multimedia joystick API (winmm) and XInput. With MSVC the source code links both libraries by itself (#pragma comment(lib, ...)); the CMake file of the library does not link them, so with MinGW the user has to link winmm and xinput to the application. If an XInput controller with the user index 0 is connected at the moment of the openJoystick(…) call, the open joystick is read through XInput (button ids 0 - 9, axis ids 0 - 5), otherwise through the multimedia joystick API.
  • The library does not use SIMD instructions, does not depend on the byte order of the host and does not use compiler extensions (standard C++17).
  • The library compiles without warnings with MSVC 2022 (x64), GCC 12 and 13 and Clang 15 and 18 (Linux x86-64), and GCC 13 (Linux aarch64).

Example

This example opens the first found joystick and reads the states of its buttons for about 10 seconds.

#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
#include "Joystick.h"



namespace
{
/// Number of cycles of reading the buttons.
constexpr int READ_CYCLES{100};

/// Delay between the cycles of reading the buttons, milliseconds.
constexpr int READ_PERIOD_MS{100};
}



int main()
{
    // Create joystick controller.
    rapidpixel::Joystick joyStickController;

    // Read information about connected joysticks.
    const std::vector<rapidpixel::JoystickInfo> joysticks =
            joyStickController.findJoysticks();
    if (joysticks.empty())
    {
        std::cout << "No joystick found" << std::endl;
        return 1;
    }

    // Open first joystick.
    const rapidpixel::JoystickInfo& info = joysticks.front();
    if (!joyStickController.openJoystick(info.id))
    {
        std::cout << "Joystick " << info.id << " is not open" << std::endl;
        return 1;
    }

    // Print joystick infos.
    std::cout << "Joystick name : " << info.name << std::endl;
    std::cout << "Number of button : " << info.numButtons << std::endl;
    std::cout << "Number of axes : " << info.numAxes << std::endl;

    // Read all available buttons one by one.
    for (int cycle = 0; cycle < READ_CYCLES; ++cycle)
    {
        for (int i = 0; i < info.numButtons; ++i)
        {
            if (joyStickController.getButtonState(i))
            {
                std::cout << "Button : " << i << " pressed" << std::endl;
            }
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(READ_PERIOD_MS));
    }

    joyStickController.closeJoystick();

    return 0;
}

Table of contents