Vortex 2728182818
Vortex 2728182818

Reputation: 180

24 bit integers that are exactly 3 bytes packed

I am writing a tool to port games written in C/C++ from the 24bit eZ80 to Windows/Linux. One of the issues is that int24_t doesn't exist on x86/x64.

typedef uint32_t uint24_t doesn't always work, especially with packed data and incrementing uint24_t* pointers. I tried _BitInt(24) in Clang C23, but I then found out that sizeof(_BitInt(24)) is 4 bytes, with -Wcast-align stating that the alignment for _BitInt(24) was also 4. I tried _ExtInt(24) but it also had a size of 4 bytes.

So far, I have only found two solutions where int24_t is exactly 3 bytes packed.

typedef struct int24_t {
    uint8_t n[3];
} int24_t;

The first being a struct. But this requires a lot of refactoring in the code, as int x = y + a * b on the eZ80 would become int24_t x = ADD_INT24(y, MUL_INT24(a, b)), which is not as readable.

class int24_t {
    private:
        uint8_t n[3];
};

The other solution I found was to use a C++ class, which is also exactly 3 bytes in Clang and GCC. Since this allows for operator overloading, expressions can be written normally int24_t x = y + a * b. However, since C and C++ are different languages, compiling C as C++ might not always work.

Are there any ways to have a packed int24_t type in C? Edit: Such that int24_t can be used with infix notation x + y instead of function notation ADD(x, y)

Upvotes: 6

Views: 480

Answers (3)

Konstantin Makarov
Konstantin Makarov

Reputation: 1376

Here's the implementation, but it's in C++:

#include <iostream>
#include <print>

struct int24_t {
    uint8_t n[3];
    int24_t() = default;
    // Converting constructor
    int24_t(int32_t x) : n{ uint8_t(x), uint8_t(x >> 8), uint8_t(x >> 16) } { }
    operator int32_t() const {
        return n[2] & 0x80
            ? (n[2] << 16) | (n[1] << 8) | n[0] | 0xFF000000
            : (n[2] << 16) | (n[1] << 8) | n[0];
    }
};

template <> // For formatted output
struct std::formatter<int24_t> : std::formatter<std::string> {
    auto format(int24_t p, format_context& ctx) const {
        return std::format_to(ctx.out(), "{}", p.operator int32_t());
    }
};

int main() {
    int32_t a32{ +1000 }, b32{ -1000 };
    int24_t a24{ a32 },   b24{ b32 };

    std::println("sizeof int24_t = {}",       sizeof int24_t);
    std::println("sizeof (int24_t[10]) = {}", sizeof(int24_t[10]));
    std::println("min int24_t: {}",           int24_t{ -8388608 });
    std::println("max int24_t: {}",           int24_t{ +8388607 });
    std::println("add: {} = {}", a32 + b32,   int24_t{ a24 + b24 });
    std::println("mul: {} = {}", a32 * b32,   int24_t{ a24 * b24 });
}

Result:

sizeof int24_t = 3
sizeof (int24_t[10]) = 30
...

Maybe it can be translated into C?

Upvotes: 1

hello_user
hello_user

Reputation: 68

Because the C language does not support operator overloading, you need to write wrapper functions (such as add_int24() and int24_mul()) to perform these operations.

Use a struct with bit fields to represent a 24-bit integer.

typedef union {
    int8_t bytes[3];
    struct {
        int32_t value : 24;
    } bits;
} int24_t;

int24_t convert(int32_t x) {
    int24_t result;
    result.bits.value = x & 0xFFFFFF;
    // handle the sign extension manually
    if (x & 0x800000) {
        result.bits.value |= ~0xFFFFFF;
    }

    return result;
}

int24_t int24_add(int24_t a, int24_t b) {
    return convert(a.bits.value + b.bits.value);
}

int24_t int24_mul(int24_t a, int24_t b) {
    return convert(a.bits.value * b.bits.value);
}

Example usage:

    int24_t a = { .bits.value = 0x123456 };
    int24_t b = { .bits.value = -1 };
    int24_t c = int24_add(a, b);
    int24_t d = int24_mul(a, b);

Upvotes: -2

virolino
virolino

Reputation: 2227

I think you are trying to solve the wrong problem. You focus on having the source code 100% compatible, instead of having the behavior 100% compatible.

I propose the following steps:

  1. Just use uint32_t for the most part.
  2. Find the places where (if any) e.g., a loop is implemented by overflowing a uint24_t. In those places, add an instruction similar to a%=(1<<24u).
  3. Find the places where that data interfaces with other special data (e.g, registers). In those places, adapt the code to take care of data sizes, if needed.
  4. Test and debug.

NOTE: You wrote: "I am writing a tool to port ..." instead of "I port ...". That goes towards my proposal: change as little as possible the original code, use the best at hand, adapt as you go. Otherwise, start writing an emulator to run the original code (hint: such emulators already exist, I guess).

Upvotes: 4

Related Questions