See also: MemoryModel, cwal_gc
cwal Data Types
This page describes the various types of Values which cwal exposes to
client code. All of these types have basic Value handle
representations (via a cwal_value
pointer) and many of them
also have higher-level representations (e.g. cwal_object
or
cwal_string
). Clients can convert freely between such
representations. Most APIs work with basic Value handles, so it is
often necessary to convert higher-level handles to the Value form
(such conversion is an O(1) operation). How to do such conversions is
demonstrated in the examples below.
Container Types and Cycles
Some types (e.g. Objects an Arrays) are generically known as "containers," meaning that they can hold other values. Such types can lead to data structures which contain cycles (circular references). That's okay. The scope-level cleanup mechanism can handle such structures and will clean them up properly, provided the client obeys the rules of the API (as described in the API docs and these wiki pages).
The following concrete types are capable of having key/value properties:
- Object
- Array (in addition to integer-indexed properties)
- Native
- Function
- Exception
- Hash Tables (largely obsolete since mid-2021, as Objects now have equivalent storage)
- Buffer (memory buffers, most often used for building strings)
Numeric
cwal differentiates between signed integer and double values. Integer precision depends on the platform and build options (16-64 bits). Double precision is as for the standard double type, but it can be configured to use long doubles as well.
Example usages:
cwal_value * i = cwal_new_integer(e, 42);
cwal_value * d = cwal_new_double(e, 42.24);
assert(cwal_value_is_integer(i));
assert(cwal_value_is_double(d));
cwal_ref(i);
cwal_ref(d);
cwal_int_t xI = cwal_value_get_integer(i);
assert( 42 == xI );
cwal_int_t xD = cwal_value_get_double(d);
assert( 42.24 == xD );
xI = cwal_value_get_integer(d) /* note the double argument! */;
assert( 42 == xI );
xD = cwal_value_get_double(i) /* note the integer argument! */;
assert( 42.0 == xD );
// Cleanup ...
cwal_unref(i);
cwal_unref(d);
Strings
Strings are, as is conventional, immutable byte arrays, and cwal generally assumes UTF8 encoding.
Example usages:
cwal_string * s = cwal_new_string(e, "hi!", 3);
cwal_value * sv = cwal_string_value(s);
assert(cwal_value_get_string(sv)==s);
char const * cstr = cwal_string_cstr(s);
cwal_ref(sv);
// Optional cleanup ...
cwal_string_unref(e, s);
// OR (DO NOT DO BOTH!)...
cwal_unref(sv);
Arrays
Arrays represent sequential lists of arbitrary values, basically analog to C arrays except that they can grow.
Example usages:
cwal_array * a = cwal_new_array(e);
cwal_value * v = cwal_new_integer(e, 42);
cwal_ref(v);
int rc = cwal_array_append(a, v);
cwal_unref(v);
assert(0==rc);
v = cwal_array_value(a);
assert(cwal_value_get_array(v)==a);
cwal_ref(v);
// Optional cleanup:
cwal_array_unref(a);
// OR (DO NOT TO BOTH!)...
cwal_unref(cwal_array_value(a));
Objects
Objects are basically collections of key/value pairs. Keys can be of almost any type. Historically, the main difference between this type has hashtables was lookup speed: O(N) vs amortized O(1). In 2021 the type of underlying property storage was made a build-time option, with the default being hashtable speed at the cost of a bit more memory (generally to within 5% of the slower model's values, thanks to heavy recycling). Neither model supports mutation during iteration, but practice has yet to reveal that as a genuine problem.
Example usages:
cwal_object * o = cwal_new_object(e);
cwal_value * v = cwal_value_new_double(e, 87.23);
cwal_ref(v);
int rc = cwal_object_set(o, "hi", 2, v );
cwal_unref(v);
assert(0==rc);
v = cwal_object_value(o);
assert(cwal_value_get_object(v)==o);
cwal_ref(v);
v = cwal_object_get(o, "hi", 2);
assert(cwal_value_is_double(v));
// Cleanup:
cwal_object_unref(o);
// OR (DO NOT TO BOTH!)...
cwal_unref(cwal_object_value(o));
Functions
Functions are C functions wrapped up in a Value handle. Their C
counterparts must implement the cwal_callback_f()
interface, and
they are called via the cwal_function_call()
family of
functions. Callbacks receive a single argument which holds all sorts
of state about the call, including the arguments list (of course), the
current scope (for fetching named parameters), the "callee" handle, a
"this object" value, and optionally arbitrary state (a void pointer)
which can be assigned by the client when the Function value is created
(along with an optional finalizer function to clean it up when the
Function Value is finalized).
Note that Functions may contain key/value pairs, just like Objects can.
Buffers
Buffers wrap up the cwal_buffer
class (a generic memory buffer) in a
Value handle, making them available for script code. This class is
intended to be used as a general-purpose byte array. Note that this
feature could also be implemented in terms of the "native" support
(see below), but i wanted a concrete implementation for some
particular scripting ideas i would like to play with.
Natives
"Native" is the generic term for a C/C++-level value which is wrapped up behind a Value handle. cwal "binds" a native handle to a Value using the following information (all provided by the client):
- A void pointer representing the native value (e.g. a C struct).
- A const void pointer which acts as a type identifier. This can be used to ensure that the client is dealing with the expected type of pointer down-stream. i.e. it provides a type-safety mechanism for the otherwise completely opaque void pointer.
- An optional finalizer function which cleans up the native pointer when the Value handle is finalized.
In principal, any type which can be passed around as a void pointer can be wrapped up in a Native Value.
Note that Natives may contain key/value pairs, just like Objects can.
Example usage:
/* The native type we want to wrap... */
typedef struct NativeType NativeType;
struct NativeType {
int x;
int y;
};
/* We use NativeType_empty as a _type identifier_. */
static const NativeType NativeType_empty = {0,0};
/* A finalizer function for NativeType values... */
static void finalizer_NativeType( cwal_engine * e, void * V ){
NativeType * n = (NativeType*)V;
printf("Freeing NativeType@%p x=%d y=%d\n", V, n->x, n->y);
cwal_free(e, V);
}
/* Example wrapper which creates a new Native value and runs
some sanity checks (which clients won't need to do - this code
is taken from cwal lib-level tests)...
*/
cwal_native * new_native(cwal_engine * e){
cwal_native * n;
cwal_value * v;
void * pCheck = NULL;
NativeType * foo;
int rc;
foo = cwal_malloc(e, sizeof(NativeType));
foo->x = 3; foo->y = 7;
n = cwal_new_native( e, foo, finalizer_NativeType, &NativeType_empty );
assert(n);
v = cwal_native_value(n);
assert(v);
assert(n == cwal_value_get_native(v));
assert( CWAL_TYPE_NATIVE == cwal_value_type_id(v) );
assert(0 == cwal_native_get(n, ""));
rc = cwal_native_fetch(n, &NativeType_empty, &pCheck);
assert(0==rc);
assert(pCheck == foo);
assert(foo == cwal_native_get(n, &NativeType_empty));
return n;
}
Exception
The Exception class (cwal_exception
) is intended to be used as a
generic error/exception reporting mechanism. Each exception instance
has an associated integer code and optional message value (an
arbitrary value), the interpretation of both of which is up to the
client. They can also contain key/value properties (e.g. that's where
stack trace info could go).
The engine provides internal support for handling a single
"propagating" exception value, propagating it down the scope stack as
scopes are pushed. This is intended to be used in conjunction with the
cwal_exception_set()
family of functions and the CWAL_RC_EXCEPTION
return value, which is reserved for signaling that an exception was
encountered.
Example usages:
cwal_value * msg = cwal_new_string_value(e, "Aarrgghh!", 0);
cwal_ref(msg);
cwal_exception * x = cwal_new_exception(e, 42, msg );
cwal_unref(msg);
int code = cwal_exception_code_get(x);
cwal_value * xV = cwal_exception_value(x);
assert(42 == code);
assert(msg == cwal_exception_message_get(x));
assert(cwal_value_get_exception(xV) == x);
Note that creating a new exception object does not "throw" the
exception. i.e. it's just another value which gets no special
treatment. To "throw" an exception, pass the exception to
cwal_exception_set()
or use one of the convenience routines, e.g.:
int rc = cwal_exception_setf( e, MyErrorCode, "Error #%d!", MyErrorCode );
assert(CWAL_RC_EXCEPTION==rc) /* on success! */;
assert(NULL != cwal_exception_get(e));
Unlike most values, the currently "thrown" exception is moved up the scope stack as exceptions are popped. When the last scope is popped, any remaining exception value is freed.
Hash Tables
Hashes are objects capable of storing both "normal" properties (as for Objects) or hashed properties, with amortized O(1) performance for all hash algorithms. They cost notably more memory than Objects, but for large property sets are (on average) faster. Unlike object properties, which use non-strict equivalence comparisons (e.g. the integer 1, double 1.0, and string "1" are equivalent), hashes use type-strict comparisons (so 1, 1.0, and "1" are three distinct keys).
Over the years, improvements in the memory recycling subsystem has cut the amortized memory cost of hashtables to within a negligible distance of Object costs. In 2021 Objects gained a compile-time option to switch their storage to the same one as hashtables use. That change effectively makes hashtables obsolete except for hypothetical uses in highly memory-constrained systems, where the "old-style" Objects might still make sense, with the caveat that that model has much slower properly lookup speed.