Skip to content

Instantly share code, notes, and snippets.

@Delaunay
Last active April 1, 2024 22:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Delaunay/2ae7deaaf49ca78cff09630acfa90aa9 to your computer and use it in GitHub Desktop.
Save Delaunay/2ae7deaaf49ca78cff09630acfa90aa9 to your computer and use it in GitHub Desktop.
#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