Pyrogenesis  13997
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
BinarySerializer.cpp
Go to the documentation of this file.
1 /* Copyright (C) 2013 Wildfire Games.
2  * This file is part of 0 A.D.
3  *
4  * 0 A.D. is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * 0 A.D. is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "precompiled.h"
19 
20 #include "BinarySerializer.h"
21 
22 #include "SerializedScriptTypes.h"
23 
24 #include "lib/alignment.h"
25 #include "ps/CLogger.h"
26 
28 #include "scriptinterface/ScriptExtraHeaders.h" // for JSDOUBLE_IS_INT32, typed arrays
29 
30 static u8 GetArrayType(uint32 arrayType)
31 {
32  switch(arrayType)
33  {
34  case js::TypedArray::TYPE_INT8:
36  case js::TypedArray::TYPE_UINT8:
38  case js::TypedArray::TYPE_INT16:
40  case js::TypedArray::TYPE_UINT16:
42  case js::TypedArray::TYPE_INT32:
44  case js::TypedArray::TYPE_UINT32:
46  case js::TypedArray::TYPE_FLOAT32:
48  case js::TypedArray::TYPE_FLOAT64:
50  case js::TypedArray::TYPE_UINT8_CLAMPED:
52  default:
53  LOGERROR(L"Cannot serialize unrecognized typed array view: %d", arrayType);
55  }
56 }
57 
59  m_ScriptInterface(scriptInterface), m_Serializer(serializer), m_Rooter(m_ScriptInterface),
60  m_ScriptBackrefsArena(1 * MiB), m_ScriptBackrefs(backrefs_t::key_compare(), ScriptBackrefsAlloc(m_ScriptBackrefsArena)), m_ScriptBackrefsNext(1)
61 {
62 }
63 
65 {
66  JSContext* cx = m_ScriptInterface.GetContext();
67 
68  switch (JS_TypeOfValue(cx, val))
69  {
70  case JSTYPE_VOID:
71  {
73  break;
74  }
75  case JSTYPE_NULL: // This type is never actually returned (it's a JS2 feature)
76  {
78  break;
79  }
80  case JSTYPE_OBJECT:
81  {
82  if (JSVAL_IS_NULL(val))
83  {
85  break;
86  }
87 
88  JSObject* obj = JSVAL_TO_OBJECT(val);
89 
90  // If we've already serialized this object, just output a reference to it
91  u32 tag = GetScriptBackrefTag(obj);
92  if (tag)
93  {
96  break;
97  }
98 
99  // Arrays are special cases of Object
100  if (JS_IsArrayObject(cx, obj))
101  {
103  // TODO: probably should have a more efficient storage format
104 
105  // Arrays like [1, 2, ] have an 'undefined' at the end which is part of the
106  // length but seemingly isn't enumerated, so store the length explicitly
107  jsuint length = 0;
108  if (!JS_GetArrayLength(cx, obj, &length))
109  throw PSERROR_Serialize_ScriptError("JS_GetArrayLength failed");
110  m_Serializer.NumberU32_Unbounded("array length", length);
111  }
112  else if (js_IsTypedArray(obj))
113  {
115 
116  js::TypedArray* typedArray = js::TypedArray::fromJSObject(obj);
117 
118  m_Serializer.NumberU8_Unbounded("array type", GetArrayType(typedArray->type));
119  m_Serializer.NumberU32_Unbounded("byte offset", typedArray->byteOffset);
120  m_Serializer.NumberU32_Unbounded("length", typedArray->length);
121 
122  // Now handle its array buffer
123  // this may be a backref, since ArrayBuffers can be shared by multiple views
124  HandleScriptVal(OBJECT_TO_JSVAL(typedArray->bufferJS));
125  break;
126  }
127  else if (js_IsArrayBuffer(obj))
128  {
130 
131  js::ArrayBuffer* arrayBuffer = js::ArrayBuffer::fromJSObject(obj);
132 
133 #if BYTE_ORDER != LITTLE_ENDIAN
134 #error TODO: need to convert JS ArrayBuffer data to little-endian
135 #endif
136 
137  u32 length = arrayBuffer->byteLength;
138  m_Serializer.NumberU32_Unbounded("buffer length", length);
139  m_Serializer.RawBytes("buffer data", (const u8*)arrayBuffer->data, length);
140  break;
141  }
142  else
143  {
144  // Find type of object
145  JSClass* jsclass = JS_GET_CLASS(cx, obj);
146  if (!jsclass)
147  throw PSERROR_Serialize_ScriptError("JS_GET_CLASS failed");
148  JSProtoKey protokey = JSCLASS_CACHED_PROTO_KEY(jsclass);
149 
150  if (protokey == JSProto_Object)
151  {
152  // Object class - check for user-defined prototype
153  JSObject* proto = JS_GetPrototype(cx, obj);
154  if (!proto)
155  throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed");
156 
157  if (m_SerializablePrototypes.empty() || !IsSerializablePrototype(proto))
158  {
159  // Standard Object prototype
161 
162  // TODO: maybe we should throw an error for unrecognized non-Object prototypes?
163  // (requires fixing AI serialization first and excluding component scripts)
164  }
165  else
166  {
167  // User-defined custom prototype
169 
170  const std::wstring& prototypeName = GetPrototypeName(proto);
171  m_Serializer.String("proto name", prototypeName, 0, 256);
172 
173  // Does it have custom Serialize function?
174  // if so, we serialize the data it returns, rather than the object's properties directly
175  JSBool hasCustomSerialize;
176  if (!JS_HasProperty(cx, obj, "Serialize", &hasCustomSerialize))
177  throw PSERROR_Serialize_ScriptError("JS_HasProperty failed");
178 
179  if (hasCustomSerialize)
180  {
181  jsval serialize;
182  if (!JS_LookupProperty(cx, obj, "Serialize", &serialize))
183  throw PSERROR_Serialize_ScriptError("JS_LookupProperty failed");
184 
185  // If serialize is null, so don't serialize anything more
186  if (!JSVAL_IS_NULL(serialize))
187  {
188  CScriptValRooted data;
189  if (!m_ScriptInterface.CallFunction(val, "Serialize", data))
190  throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed");
191  HandleScriptVal(data.get());
192  }
193  break;
194  }
195  }
196  }
197  else if (protokey == JSProto_Number)
198  {
199  // Standard Number object
201  // Get primitive value
202  jsdouble d;
203  if (!JS_ValueToNumber(cx, val, &d))
204  throw PSERROR_Serialize_ScriptError("JS_ValueToNumber failed");
206  break;
207  }
208  else if (protokey == JSProto_String)
209  {
210  // Standard String object
212  // Get primitive value
213  JSString* str = JS_ValueToString(cx, val);
214  if (!str)
215  throw PSERROR_Serialize_ScriptError("JS_ValueToString failed");
216  ScriptString("value", str);
217  break;
218  }
219  else if (protokey == JSProto_Boolean)
220  {
221  // Standard Boolean object
223  // Get primitive value
224  JSBool b;
225  if (!JS_ValueToBoolean(cx, val, &b))
226  throw PSERROR_Serialize_ScriptError("JS_ValueToBoolean failed");
227  m_Serializer.Bool("value", b == JS_TRUE);
228  break;
229  }
230  else
231  {
232  // Unrecognized class
233  LOGERROR(L"Cannot serialise JS objects with unrecognized class '%hs'", jsclass->name);
235  }
236  }
237 
238  // Find all properties (ordered by insertion time)
239 
240  // (Note that we don't do any rooting, because we assume nothing is going to trigger GC.
241  // I'm not absolute certain that's necessarily a valid assumption.)
242 
243  AutoJSIdArray ida (cx, JS_Enumerate(cx, obj));
244  if (!ida.get())
245  throw PSERROR_Serialize_ScriptError("JS_Enumerate failed");
246 
247  m_Serializer.NumberU32_Unbounded("num props", (uint32_t)ida.length());
248 
249  for (size_t i = 0; i < ida.length(); ++i)
250  {
251  jsid id = ida[i];
252 
253  jsval idval, propval;
254 
255  // Get the property name as a string
256  if (!JS_IdToValue(cx, id, &idval))
257  throw PSERROR_Serialize_ScriptError("JS_IdToValue failed");
258  JSString* idstr = JS_ValueToString(cx, idval);
259  if (!idstr)
260  throw PSERROR_Serialize_ScriptError("JS_ValueToString failed");
261 
262  ScriptString("prop name", idstr);
263 
264  // Use LookupProperty instead of GetProperty to avoid the danger of getters
265  // (they might delete values and trigger GC)
266  if (!JS_LookupPropertyById(cx, obj, id, &propval))
267  throw PSERROR_Serialize_ScriptError("JS_LookupPropertyById failed");
268 
269  HandleScriptVal(propval);
270  }
271 
272  break;
273  }
274  case JSTYPE_FUNCTION:
275  {
276  // We can't serialise functions, but we can at least name the offender (hopefully)
277  std::wstring funcname(L"(unnamed)");
278  JSFunction* func = JS_ValueToFunction(cx, val);
279  if (func)
280  {
281  JSString* string = JS_GetFunctionId(func);
282  if (string)
283  {
284  size_t length;
285  const jschar* ch = JS_GetStringCharsAndLength(cx, string, &length);
286  if (ch && length > 0)
287  funcname = std::wstring(ch, ch + length);
288  }
289  }
290 
291  LOGERROR(L"Cannot serialise JS objects of type 'function': %ls", funcname.c_str());
293  }
294  case JSTYPE_STRING:
295  {
297  ScriptString("string", JSVAL_TO_STRING(val));
298  break;
299  }
300  case JSTYPE_NUMBER:
301  {
302  // For efficiency, handle ints and doubles separately.
303  if (JSVAL_IS_INT(val))
304  {
306  m_Serializer.NumberI32_Unbounded("value", (int32_t)JSVAL_TO_INT(val));
307  }
308  else
309  {
310  ENSURE(JSVAL_IS_DOUBLE(val));
311 
312  // If the value fits in an int, serialise as an int
313  jsdouble d = JSVAL_TO_DOUBLE(val);
314  int32_t i;
315  if (JSDOUBLE_IS_INT32(d, &i))
316  {
318  m_Serializer.NumberI32_Unbounded("value", i);
319  }
320  // Otherwise serialise as a double
321  else
322  {
325  }
326  }
327  break;
328  }
329  case JSTYPE_BOOLEAN:
330  {
332  JSBool b = JSVAL_TO_BOOLEAN(val);
333  m_Serializer.NumberU8_Unbounded("value", b ? 1 : 0);
334  break;
335  }
336  case JSTYPE_XML:
337  {
338  LOGERROR(L"Cannot serialise JS objects of type 'xml'");
340  }
341  default:
342  {
343  debug_warn(L"Invalid TypeOfValue");
345  }
346  }
347 }
348 
349 void CBinarySerializerScriptImpl::ScriptString(const char* name, JSString* string)
350 {
351  JSContext* cx = m_ScriptInterface.GetContext();
352  size_t length;
353  const jschar* chars = JS_GetStringCharsAndLength(cx, string, &length);
354 
355  if (!chars)
356  throw PSERROR_Serialize_ScriptError("JS_GetStringCharsAndLength failed");
357 
358 #if BYTE_ORDER != LITTLE_ENDIAN
359 #error TODO: probably need to convert JS strings to little-endian
360 #endif
361 
362  // Serialize strings directly as UTF-16, to avoid expensive encoding conversions
363  m_Serializer.NumberU32_Unbounded("string length", (uint32_t)length);
364  m_Serializer.RawBytes(name, (const u8*)chars, length*2);
365 }
366 
368 {
369  // To support non-tree structures (e.g. "var x = []; var y = [x, x];"), we need a way
370  // to indicate multiple references to one object(/array). So every time we serialize a
371  // new object, we give it a new non-zero tag; when we serialize it a second time we just
372  // refer to that tag.
373  //
374  // The tags are stored in a map. Maybe it'd be more efficient to store it inline in the object
375  // somehow? but this works okay for now
376 
377  std::pair<backrefs_t::iterator, bool> it = m_ScriptBackrefs.insert(std::make_pair(obj, m_ScriptBackrefsNext));
378 
379  // If it was already there, return the tag
380  if (!it.second)
381  return it.first->second;
382 
383  // If it was newly inserted, we need to make sure it gets rooted
384  // for the duration that it's in m_ScriptBackrefs
385  m_Rooter.Push(it.first->first);
387  // Return a non-tag number so callers know they need to serialize the object
388  return 0;
389 }
390 
392 {
393  return m_SerializablePrototypes.find(prototype) != m_SerializablePrototypes.end();
394 }
395 
396 std::wstring CBinarySerializerScriptImpl::GetPrototypeName(JSObject* prototype)
397 {
398  std::map<JSObject*, std::wstring>::iterator it = m_SerializablePrototypes.find(prototype);
399  ENSURE(it != m_SerializablePrototypes.end());
400  return it->second;
401 }
402 
403 void CBinarySerializerScriptImpl::SetSerializablePrototypes(std::map<JSObject*, std::wstring>& prototypes)
404 {
405  m_SerializablePrototypes = prototypes;
406 }
#define u8
Definition: types.h:39
ScriptInterface & m_ScriptInterface
#define LOGERROR
Definition: CLogger.h:35
Serialization interface; see serialization overview.
Definition: ISerializer.h:120
void NumberU8_Unbounded(const char *name, uint8_t value)
Serialize a number.
Definition: ISerializer.h:150
std::map< JSObject *, u32, std::less< JSObject * >, ScriptBackrefsAlloc > backrefs_t
void ScriptString(const char *name, JSString *string)
#define ENSURE(expr)
ensure the expression &lt;expr&gt; evaluates to non-zero.
Definition: debug.h:282
static u8 GetArrayType(uint32 arrayType)
void SetSerializablePrototypes(std::map< JSObject *, std::wstring > &prototypes)
static const size_t MiB
Definition: alignment.h:72
void Bool(const char *name, bool value)
Serialize a boolean.
Definition: ISerializer.h:199
std::map< JSObject *, std::wstring > m_SerializablePrototypes
CBinarySerializerScriptImpl(ScriptInterface &scriptInterface, ISerializer &serializer)
void NumberU32_Unbounded(const char *name, uint32_t value)
Serialize a number.
Definition: ISerializer.h:171
size_t length() const
Definition: ScriptVal.cpp:84
void String(const char *name, const std::wstring &value, uint32_t minlength, uint32_t maxlength)
Serialize a Unicode string.
Definition: ISerializer.cpp:82
void Push(JSObject *obj)
Definition: AutoRooters.h:38
void RawBytes(const char *name, const u8 *data, size_t len)
Serialize a stream of bytes.
bool CallFunction(jsval val, const char *name, R &ret)
Call the named property on the given object, with return type R and 0 arguments.
void NumberDouble_Unbounded(const char *name, double value)
Serialize a number.
Definition: ISerializer.h:186
JSIdArray * get() const
Definition: ScriptVal.cpp:79
fully STL-compatible allocator that simply draws upon another Allocator.
#define u32
Definition: types.h:41
unsigned int uint32_t
Definition: wposix_types.h:53
jsval get() const
Returns the current value (or JSVAL_VOID if uninitialised).
Definition: ScriptVal.cpp:45
Abstraction around a SpiderMonkey JSContext.
void NumberI32_Unbounded(const char *name, int32_t value)
Serialize a number.
Definition: ISerializer.h:176
#define debug_warn(expr)
display the error dialog with the given text.
Definition: debug.h:324
std::wstring GetPrototypeName(JSObject *prototype)
JSContext * GetContext() const
u32 GetScriptBackrefTag(JSObject *obj)
RAII wrapper for JSIdArray*.
Definition: ScriptVal.h:84
bool IsSerializablePrototype(JSObject *prototype)