-
Notifications
You must be signed in to change notification settings - Fork 756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
README: mention that library is not thread-safe #498
Conversation
It's such a big deal, it should be among the first things a person gotta know! I certainly didn't, it led to hours of frustration for why when I have a bunch of clients communicating with a server (a thread per client), sometimes I'm getting packets duplicated, even though clients sent them correct. It costed lots of energy to pin down to deserialization, and no, the CEREAL_THREAD_SAFE didn't help — probably because, per the linked docs, you still not allowed to access the same archive from multiple threads. Signed-off-by: Konstantine Kharlamov <Hi-Angel@yandex.ru>
What? Frack! |
Interestingly, it seems, Cereal-like API is unlikely to be possible without a global object (i.e. thread-safe). I have tried making a backend to match the API, so I could use it without modifications to my code, and have come up with this crazy construction. This doesn't work, because the compiler fails to infer the first of the variadic arguments. I don't see any ways to proceed.
|
I am not sure why you are declaring SerInternals locally but this works:
namespace detail {
struct SerInternals {
vector<char> vec;
SerInternals(vector<char>& vec) : vec(vec) {}
template<typename T>
void archive(T t) {
//todo: use std::copy instead
for (unsigned i = 0; i < sizeof(t); ++i)
vec.push_back(*(((char*)&t)+i));
}
template<typename T, typename... Args>
void archive(T fst, Args... args) {
//todo: use std::copy instead
for (unsigned i = 0; i < sizeof(fst); ++i)
vec.push_back(*(((char*)&fst)+i));
archive(args...);
}
};
}
template<class ToSer>
vector<char> serialize2(ToSer obj){
vector<char> ret;
detail::SerInternals tmp{ret};
obj.save([&](auto&&... a) { tmp.archive(a...); });
return ret;
} If you insist on keeping it local you can use recursive variadic lambda (tough it requires C++17). Without if constexpr it requires an overload, there were implementations that provide overloads on lambdas (e.g. std::visit) though I am not sure whether they can be implemented solely using C++11 or earlier. template<class ToSer>
vector<char> serialize2(ToSer obj){
vector<char> ret;
auto archive = [&](auto&&... t){
// The below is necessary for recursion
auto a_impl = [&](auto& self, auto&& fst, auto&&... args) {
for (unsigned i = 0; i < sizeof(fst); ++i)
ret.push_back(*(((char*)&fst)+i));
if constexpr (sizeof...(args)){
self(self, args...);
}
};
return a_impl(a_impl, t...);
};
obj.save(archive);
return ret;
} |
Ha! Thanks, cool, this indeed does! The reason I was declaring it locally is because it took a looong way there, and I simply missed the opportunity to declare it as usual, and then create an object — thus getting the thread-safe serialization. |
Boost.Phoenix was polymorphic templateable lambdas in the C++98 era, so yes it is possible. :-) |
Okay, FWIW, I managed to build the serialization part. The next tricky problem was the need to serialize embedded structs with their own
upd: after wiring up into a real project I found problems with enums. So I fixed the code to deal with this stuff. |
FTR, here's the acc. deserialization part, hopefully someone find it useful too. I wrote it on prev. week, but wanted to make sure everything works. BTW, the duplication problem turned out to be a race irrelevant to (de)serialization (funny story btw), but still I glad I uncovered that Cereal is non-thread safe before the project grew up much bigger. #include <iostream>
#include <type_traits>
#include <variant>
#include <vector>
using namespace std;
struct DeSerException: public exception {
const char* explanation;
DeSerException(const char* text) : explanation(text) {}
virtual const char* what() const throw() {
return explanation;
}
};
// like std::decay, but removes both constantess, references, pointers.
template<class T>
struct decay2 {
typedef typename std::remove_cv<T>::type nocv_T;
typedef typename std::remove_reference<nocv_T>::type noref_nocv_T;
typedef typename std::remove_pointer<noref_nocv_T>::type noref_nocv_noptr;
typedef typename std::remove_reference<noref_nocv_noptr>::type noref_nocv_T2;
typedef typename std::remove_pointer<noref_nocv_T2>::type noref_nocv_noptr2;
typedef noref_nocv_noptr2 type;
};
class DeSerInternals {
const char*& start, *&past_end;
template<typename T>
void deserialize_elem(T& elem) {
if constexpr (is_fundamental<typename decay2<T>::type>::value
|| is_enum<typename decay2<T>::type>::value) {
if (0 > past_end - start || sizeof(elem) > (size_t) (past_end - start))
throw DeSerException{"Size of object being deserialized is bigger than the byte-vector!"};
auto fst = start, past_lst = start + sizeof(elem);
copy(fst, past_lst, ((char*)&elem));
start = past_lst;
} else { // it's a struct
DeSerInternals tmp{start, past_end};
if constexpr (is_pointer<T>::value)
elem->load([&tmp](auto&&... a) { tmp.archive(a...); });
else
elem.load([&tmp](auto&&... a) { tmp.archive(a...); });
}
}
public:
constexpr DeSerInternals(const char*& start, const char*& past_end) : start(start), past_end(past_end) {}
template<typename T>
constexpr void archive(T& x) {
deserialize_elem(x);
}
template<typename T, typename... Args>
constexpr void archive(T& x, Args&... xs) {
deserialize_elem(x);
archive(xs...);
}
};
template<class DeSer>
variant<monostate, DeSer> deserialize(const char dat[], uint sz_dat){
const char* fst = dat, *past_lst = dat + sz_dat;
DeSer ret;
DeSerInternals tmp{fst, past_lst};
try{ ret.load([&tmp](auto&&... a) { tmp.archive(a...); }); } catch (DeSerException) {
return monostate{};
}
return {ret};
}
struct Foo {
char a;
uint16_t b;
enum : uint8_t {
enum_member1
} c;
template<class Archive>
void load(const Archive&& archive) {
archive(a, b, c);
}
};
int main() {
vector<char> raw = {1, 2,0, 3};
variant<monostate, Foo> mb_Foo = deserialize<Foo>(raw.data(), raw.size());
if (holds_alternative<monostate>(mb_Foo))
puts("Deserialization failed!");
else {
Foo& obj = get<Foo>(mb_Foo);
printf("a = %u, b = %u, c = %u\n", obj.a, obj.b, obj.c);
}
}
upd: repeating code moved out to a separate function, and also enabled syntax highlight |
I'll update the documentation to more clearly reflect the limitations of cereal and threading. |
It's such a big deal, it should be among the first things a person gotta know!
I certainly didn't, it led to hours of frustration for why when I have a bunch(upd: the threading problem turned to be irrelevant to Cereal. The point stands though.)of clients communicating with a server (a thread per client), sometimes I'm
getting packets duplicated, even though clients sent them correct. It costed
lots of energy to pin down to deserialization, and no, the CEREAL_THREAD_SAFE
didn't help — probably because, per the linked docs, you still not allowed to
access the same archive from multiple threads.
It's such a surprise — why would a serialization library, aiming to be
header-only through C++11 features, use a global state? It's a rhetoric
question, maybe there is a reason, but let's at least make sure it's less
likely for new peoples to fall into threading trap.