Reputation: 180
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
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
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
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:
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