Last active
August 3, 2023 04:55
-
-
Save JohnnyonFlame/5ccd797d5681bf691eb18e581ddb3239 to your computer and use it in GitHub Desktop.
Compile-time parsed AOB patterns example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# Do what the fuck you want to public license | |
Version 2, December 2004 | |
Copyright (C) `2021` `Johnny on Flame` | |
Everyone is permitted to copy and distribute verbatim or modified | |
copies of this license document, and changing it is allowed as long | |
as the name is changed. | |
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE | |
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION | |
0. You just DO WHAT THE FUCK YOU WANT TO. | |
*/ | |
/* | |
This little example allows you to define Array of Byte scans with masks on C++ with | |
compile time pattern parsing, allowing errors to be caught and reported during compilation, | |
and thus no parsing happens during runtime. | |
It's mostly an excuse to study and understand constexpr a little better. | |
*/ | |
#include <string_view> | |
#include <iostream> | |
#include <vector> | |
using namespace std; | |
// Wrapper to transform string literals into something that can be passed as | |
// a template parameter. | |
// String literals can't be used as template parameters, but arrays with chars can, | |
// this wrapper explodes the literals into arrays. | |
template<int N> | |
struct _str { | |
char value[N]; | |
const int size = N; | |
constexpr _str(const char (&str)[N]) { | |
copy_n(str, N, value); | |
} | |
}; | |
// isxdigit isn't constexpr-able... | |
constexpr int ishex(char c) { | |
return | |
((c) >= 'a' && (c) <= 'f') || | |
((c) >= 'A' && (c) <= 'F') || | |
((c) >= '0' && (c) <= '9'); | |
} | |
// meh | |
constexpr uint8_t hex_to_int(char a, char b) | |
{ | |
#define doit(c) ( \ | |
((c) >= 'a' && (c) <= 'f') ? (c) - 'a' + 10: \ | |
((c) >= 'A' && (c) <= 'F') ? (c) - 'A' + 10: \ | |
(c) - '0' \ | |
) | |
return doit(a) << 4 | doit(b); | |
} | |
// Wrapper struct to copy back results from the lambda in a constexpr form. | |
// This is necessary to ensure that the final state of our calculation is all available | |
// in a neatly constexpr'd form, because otherwise, arguments and variables can't be considered | |
// constant expressions and won't be usable on template arguments and static_assert arguments. | |
struct result_t { | |
int state; | |
int size; | |
int col; | |
using storage_type = uint8_t[2048]; | |
storage_type bytes; | |
storage_type mask; | |
constexpr result_t(int state, int size, int col, const decltype(bytes) &bytes, const decltype(mask) &mask) | |
: state(state), size(size), col(col) | |
{ | |
copy_n(bytes, sizeof(bytes), this->bytes); | |
copy_n(mask, sizeof(mask), this->mask); | |
} | |
}; | |
// The actual compile-time built/guaranteed AOB pattern and mask. | |
template <int N> | |
struct aob_t { | |
const int size = N; | |
uint8_t bytes[N]; | |
uint8_t mask[N]; | |
template <typename T> | |
constexpr aob_t(const T &t) { | |
copy_n(t.bytes, N, bytes); | |
copy_n(t.mask, N, mask); | |
} | |
uint8_t *scan(uint8_t *start, uint8_t *end) const { | |
for (uint8_t *ptr = start; ptr < end; ptr++) { | |
int pos = 0; | |
for (uint8_t *cont = ptr; cont < end; cont++) { | |
if (mask[pos] == 1 || bytes[pos] == *cont) { | |
if (++pos >= size) | |
return (uint8_t*)ptr; | |
} else { | |
break; | |
} | |
} | |
} | |
return NULL; | |
}; | |
}; | |
template <int ISERR = 0> | |
constexpr void LAYER() | |
{ | |
static_assert(ISERR == 0); | |
} | |
template <_str str1, int ISERR = 0> | |
constexpr void ERROR() | |
{ | |
LAYER<ISERR>(); | |
} | |
template <typename T> | |
int constexpr _strlen(T str) | |
{ | |
return *str ? 1 + _strlen(str + 1) : 0; | |
} | |
// Format and pass error around to display | |
template <_str pattern, int COLUMN, int LAST_STATE = 0> | |
constexpr void AOB_PARSE_CHECK_ERROR() | |
{ | |
constexpr auto err = []() { | |
// choose error type | |
constexpr const char *msg = []() { | |
switch (LAST_STATE) { | |
case -1: return ": Expected hex or <?>, got <%> instead."; | |
case -2: return ": Expected hex, got <%> instead."; | |
case -3: return ": Expected separator or <?>, got <%> instead."; | |
default: return ": Unknown error message."; | |
} | |
}(); | |
// Create number string the naive way... | |
constexpr auto colno = []() { | |
char colno[100] = ""; | |
int i = COLUMN; | |
do { | |
colno[_strlen(colno)] = (i % 10) + '0'; | |
colno[_strlen(colno)] = '\0'; | |
i /= 10; | |
} while(i > 1 || i < -1); | |
return _str(colno); | |
}(); | |
// final string concat & format | |
constexpr int len = _strlen(msg); | |
constexpr int len2 = _strlen(colno.value); | |
#define PREAMBLE "Column " | |
char err[len + sizeof(PREAMBLE) + len2] = {}; | |
copy_n(PREAMBLE, sizeof(PREAMBLE), err); | |
copy_n(colno.value, len2, err + _strlen(err)); | |
copy_n(msg, len, err + _strlen(err)); | |
for (int i = 0; i < _strlen(err); i++) { | |
if (err[i] == '%') { | |
err[i] = pattern.value[COLUMN]; | |
} | |
} | |
return _str(err); | |
}(); | |
// This will cause gcc to print the formatted error. | |
ERROR<err, LAST_STATE != 0>(); | |
} | |
template <_str pattern> | |
constexpr auto aob_parse() | |
{ | |
#define head pattern.value[cur] | |
// The lambda is used in order to build the "result_t" wrapper, this guarantees | |
// that all variables inside the struct are "constant expressions" compatible, and thus | |
// we can peek at the final state and pattern size during compile time. | |
constexpr auto ret = [&]() -> auto { | |
int state = 0; | |
int size = 0; | |
result_t::storage_type bytes = {}; | |
result_t::storage_type mask = {}; | |
int cur; | |
for (cur = 0; head != '\0' && state >= 0; cur++) { | |
switch(state) { | |
//State 0: .. | |
case 0: | |
if (ishex(head)) state = 1; //X. | |
else if (head == '?') state = 2; //?. | |
else if (head == ' ') state = 0; //.. | |
else state = -1; | |
break; | |
//State 1: X. | |
case 1: | |
bytes[size++] = hex_to_int(pattern.value[cur-1], pattern.value[cur]); | |
if (ishex(head)) state = 0; //XX | |
else state = -2; | |
break; | |
//State 2: ?. | |
case 2: | |
mask[size++] = 1; | |
if (head == '?') state = 0; //?? | |
else if (head == ' ') state = 0; //?s | |
else state = -3; | |
break; | |
} | |
if (state < 0) | |
break; | |
} | |
// This cannot compile yet due to it not "being a constant expression.": | |
// static_assert(state == 0, "Invalid AOB Syntax"); | |
// - <source>: error: static_assert expression is not an integral constant expression | |
// So, from here on out, state, size, bytes and mask will be wrapped in 'result_t' and | |
// available for usage on "constant expressions" such as static_assert and etc on aob_parse. | |
return result_t(state, size, cur, bytes, mask); | |
}(); | |
// Warn users about malformed AOB syntaxes during compile time, now possible due to the 'result_t' wrapper. | |
AOB_PARSE_CHECK_ERROR<pattern, ret.col, ret.state>(); | |
// Can also use the wrapper members as template parameters. | |
return aob_t<ret.size>(ret); | |
#undef head | |
} | |
// Different instances of the templated function have their own storage, we're going to | |
// use that fact to keep the pattern static. | |
template <_str pattern, typename T> | |
uint8_t *aob_scan(T start, T end) { | |
static constexpr auto aob = aob_parse<pattern>(); | |
return aob.scan((uint8_t *)start, (uint8_t*)end); | |
} | |
int main() | |
{ | |
const char str[] = "\x01\x02\x03\xFF\xFF\xAA\xBA\xAF\x00\x00"; | |
uint8_t *ptr1 = aob_scan<"FF AA ?? AF 00">(str, str + sizeof(str)); | |
uint8_t *ptr2 = aob_scan<"02 03 ?? ff AA">(str, str + sizeof(str)); | |
printf("ptr1 %p\n", (uint8_t *)((uintptr_t)ptr1 - (uintptr_t)str)); | |
printf("ptr2 %p\n", (uint8_t *)((uintptr_t)ptr2 - (uintptr_t)str)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
x86-64 clang 13.0.0, -std=c++20 -O3 -ggdb
Interestingly, at O3 clang will determine these are all constants and precalculate the results, very neat code generation.
x86-64 clang 13.0.0, -std=c++20 -O2 -ggdb
At O2, it will instead create jump tables to scan the range, this is what a real world situation will look like with dynamic things.
x86-64 gcc 11.2, -std=c++20 -O3 -ggdb
At this level it doesn't look much different than clang's O2. A win for clang here.