Last active
April 1, 2024 22:53
-
-
Save Delaunay/2ae7deaaf49ca78cff09630acfa90aa9 to your computer and use it in GitHub Desktop.
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
#include <cassert> | |
#include <cmath> | |
#include <cstdint> | |
#include <functional> | |
#include <iostream> | |
#include <vector> | |
template <typename V> using Array = std::vector<V>; | |
template <typename... Args> using Tuple = std::tuple<Args...>; | |
using String = std::string; | |
struct Value; | |
using uint64 = std::uint64_t; | |
using int64 = std::int64_t; | |
using uint32 = std::uint32_t; | |
using int32 = std::int32_t; | |
using uint16 = std::uint16_t; | |
using int16 = std::int16_t; | |
using uint8 = std::uint8_t; | |
using int8 = std::int8_t; | |
using float32 = float; | |
using float64 = double; | |
using Function = Value (*)(Array<Value> const &); | |
struct Struct { | |
uint64 id; // this id double the size of the pointer | |
// when it is unlikely for its entire ranged to be used | |
void *ptr; | |
// we could save the deleter here and remove ManagedVariable | |
// at the price of a bigger overhead | |
// void(*deleter)(void*) = nullptr | |
}; | |
struct None {}; | |
#define TYPES(X) \ | |
X(uint64, u64) \ | |
X(int64, i64) \ | |
X(uint32, u32) \ | |
X(int32, i32) \ | |
X(uint16, u16) \ | |
X(int16, i16) \ | |
X(uint8, u8) \ | |
X(int8, i8) \ | |
X(float32, f32) \ | |
X(float64, f64) \ | |
X(Struct *, obj) \ | |
X(Function, fun) \ | |
X(None, none) | |
enum class ValueTypes { | |
#define ENUM(type, name) name, | |
TYPES(ENUM) | |
#undef ENUM | |
Max | |
}; | |
#if 1 | |
inline int _new_type() { | |
static int counter = int(ValueTypes::Max); | |
return ++counter; | |
} | |
template <typename T> struct _type_id { | |
static int id() { | |
static int _id = _new_type(); | |
return _id; | |
} | |
}; | |
// | |
// Small type reserve their ID | |
// | |
#define TYPEID_SPEC(type, name) \ | |
template <> struct _type_id<type> { \ | |
static constexpr int id() { return int(ValueTypes::name); } \ | |
}; | |
TYPES(TYPEID_SPEC) | |
#undef TYPEID_SPEC | |
template <typename T> int type_id() { return _type_id<T>::id(); } | |
#else | |
template <typename T> constexpr int type_id() { | |
return reinterpret_cast<uint64>(&type_id<T>); | |
} | |
#endif | |
template <typename T> struct Getter { static T get(Value &v); }; | |
// | |
// Simple dynamic value that holds small value on the stack | |
// and objects on the heap, the value is cheap to copy. | |
// it does not handle auto deletion (like a variant might) | |
// | |
// sizeof(value) 8 | |
// sizeof(tag) 4 | |
// | |
// One thing to make it faster would be to insert the tag inside the value | |
// so the value in total would be 64bit max. | |
// | |
// we would only lose precision on the u64, i64, f64 | |
// IIRC pointer actual range is 48bit so the impact could be limited | |
// | |
// although tagging a floating point might be a bit arcane | |
// | |
// currently it is probably around 96bit | |
// | |
// 13 types => 4 bit required for the tag | |
// we could remove the lower precision to reduce the number of types | |
// | |
// NOTE: | |
// | |
// I might want some math datatype later like Vec2 & Vec3 (position) & Vec4 | |
// (color) then they would blow up the Value size anyway | |
// | |
// For object we have to be cautious because the Value will be copied | |
// | |
struct Value { | |
union Holder { | |
#define ATTR(type, name) type name; | |
TYPES(ATTR) | |
#undef ATTR | |
}; | |
Holder value; | |
int tag; | |
Value() : tag(type_id<None>()) {} | |
#define CTOR(type, name) \ | |
Value(type name) : tag(type_id<type>()) { value.name = name; } | |
TYPES(CTOR) | |
#undef CTOR | |
template <typename T> bool is_type() const { return type_id<T>() == tag; } | |
template <typename T> T as() { return Getter<T>::get(*this); } | |
template <typename T> T as() const { return Getter<T>::get(*this); } | |
}; | |
template <typename T> T Getter<T>::get(Value &v) { | |
using Underlying = std::remove_pointer_t<T>; | |
static T def = nullptr; | |
if (v.is_type<Struct *>()) { | |
Struct *obj = v.as<Struct *>(); | |
if (obj->id == type_id<Underlying>()) { | |
return static_cast<T>(obj->ptr); | |
} | |
} | |
return def; | |
} | |
#define GETTER(type, name) \ | |
template <> struct Getter<type> { \ | |
static type get(Value &v) { return v.value.name; }; \ | |
}; | |
TYPES(GETTER) | |
#undef GETTER | |
std::ostream &operator<<(std::ostream &os, None const &v) { | |
return os << "None"; | |
} | |
std::ostream &operator<<(std::ostream &os, Value const &v) { | |
switch (ValueTypes(v.tag)) { | |
#define CASE(type, name) \ | |
case ValueTypes::name: \ | |
return os << v.value.name; | |
TYPES(CASE) | |
#undef CASE | |
case ValueTypes::Max: | |
break; | |
} | |
return os << "obj"; | |
} | |
// | |
// Automatic Function Wrapper | |
// | |
void free_value(Value val, void (*deleter)(void *) = nullptr); | |
template <typename T> void destructor(void *ptr) { ((T *)(ptr))->~T(); } | |
// | |
// Custom object wrapper | |
// | |
// Some lib take care of the allocation for us | |
// so this would not work, and we would have to allocate 2 twice (once from | |
// the lib & another for us) | |
// | |
template <typename T, typename... Args> Value make_value(Args... args) { | |
// | |
// Point(float x, float y) could fit inside the value itself | |
// but where would the tag go, we need to know this is an object | |
// and we need to know which object it is | |
// | |
// | |
// if (sizeof(T) <= sizeof(Value::Holder)) { | |
// Value value; | |
// uint8* memory = (uint8*)(&value.value); | |
// new (memory) T(args...); | |
// return value; | |
// } | |
using Underlying = std::remove_pointer_t<T>; | |
// up to the user to free it correctly | |
void *memory = malloc(sizeof(Underlying) + sizeof(Struct)); | |
Struct *data = new (memory) Struct(); | |
data->ptr = static_cast<uint8_t *>(memory) + sizeof(Struct); | |
data->id = type_id<Underlying>(); | |
new (data->ptr) Underlying(args...); | |
// I could make the make_value return the deleter as well | |
// It would be up to the user to manager them | |
// | |
// auto deleter = [](Value val) { | |
// free_value(val, destructor<T>); | |
// }; | |
return Value(data); | |
} | |
template <typename T, typename... Args> | |
Value make_value(T *raw /*, void(*custom_free)(void*)*/) { | |
// up to you to know how to free this | |
uint8 *memory = (uint8 *)malloc(sizeof(Struct)); | |
Struct *data = new (memory) Struct(); | |
data->ptr = raw; | |
data->id = type_id<T>(); | |
// auto deleter = [custom_free](Value val) { | |
// free_value(val, custom_free); | |
// }; | |
return Value(data); | |
} | |
void free_value(Value val, void (*deleter)(void *)) { | |
// we don't know the type here | |
// we have to tag the value to know no memory was allocated | |
// if (sizeof(T) <= sizeof(Value::Holder)) { | |
// return; | |
// } | |
if (val.is_type<Struct*>()) { | |
Struct *obj = val.as<Struct *>(); | |
// obj != nullptr just there for now probably to be removed | |
// rely on the assert to make sure the assumption holds | |
// we should not have double delete with the GC anyway | |
if (deleter != nullptr && obj != nullptr) { | |
assert(obj != nullptr && "double delete"); | |
// in case of a C++ object the desctuctor need to be called manually | |
// the memory itself will get cleaned later | |
deleter(obj->ptr); | |
} | |
// One allocation for both | |
free(obj); | |
// NOTE: this only nullify current value so other copy of this value | |
// might still think the value is valid | |
// one thing we can do is allocate the memory using a pool | |
// on free the memory returns to the pool and it is marked as invalid | |
// copied value will be able to check for the mark | |
val.value.obj = nullptr; // just in case | |
} | |
} | |
// | |
// Not convinced this is useful | |
// | |
// In our use case we manage the lifetime of the value so | |
// there might be a use to store the deleter along side the value | |
// but calling it in the destructor is not that interesting | |
// | |
// | |
struct ManagedValue { | |
Value val; | |
void (*deleter)(void *) = nullptr; | |
~ManagedValue() { free_value(val, deleter); } | |
}; | |
template <typename T, typename... Args> | |
ManagedValue make_managed_value(Args... args) { | |
return ManagedValue{make_value<T>(args...), destructor<T>}; | |
} | |
// | |
// Example | |
// | |
struct Point { | |
Point(float x, float y) : x(x), y(y) {} | |
~Point() { std::cout << "Destructor called" << std::endl; } | |
float x, y; | |
float distance() { return sqrt(x * x + y * y); } | |
}; | |
struct ScriptObject { | |
// Name - Value | |
// We would like to get rid of the name if possible | |
// during SEMA we can resolve the name to the ID | |
// so we would never have to lookup by name | |
// If some need the name at runtime this is reflection stuff | |
// and that would be handled by a different datastructure | |
// for now it will have to do | |
Array<Tuple<String, Value>> attributes; | |
}; | |
// | |
// Invoke a script function with native values or script values | |
// | |
template <typename... Args> Value invoke(Value fun, Args... args) { | |
Array<Value> value_args = {Value(args)...}; | |
return fun.as<Function>()(value_args); | |
} | |
int main() { | |
std::cout << "size: " << sizeof(Value) << std::endl; | |
Value a(10); | |
Value b(11); | |
int r = a.as<int>() + b.as<int>(); | |
// Manual Wrap | |
// | |
// Wrap a native function to be called with script values | |
Value distance([](Array<Value> const& args) -> Value { | |
Value a = args[0]; | |
return Value(a.as<Point*>()->distance()); | |
}); | |
// Auto wrap | |
{ | |
// manual free | |
Value p = make_value<Point>(1, 2); | |
std::cout << "invoke: " << invoke(distance, p) << std::endl; | |
free_value(p, destructor<Point>); | |
} | |
{ | |
// auto free | |
ManagedValue p = make_managed_value<Point>(1, 2); | |
} | |
std::cout << "size: " << sizeof(Value) << std::endl; | |
return (a.as<int>() + r) * 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment