402 25.IntrospectionforC++GameEngines
#include <introspection/introspection.h>
struct UserInfo
{
std::string name;
std::string email;
std::string password;
int shoe_size;
INTROSPECTION
(
UserInfo,
MEMBER(name, "user name")
MEMBER(email, "e-mail address")
MEMBER(password, "user password")
MEMBER(shoe_size,
introspection::int_range("shoe size (European)", 30, 50))
);
};
Listing 25.3. Introspection example (the simple way).
priate introspection information. Compared to the previous example, this is a sig-
nificant improvement in both maintainability and ease of use!
The new example uses the standard C++ library string class for storage of
strings. This is because, when dealing with strings of indeterminate size (such as
those found in files, network packets, or graphical user interface editors),
memory allocation and deallocation can otherwise become a problem. Because
every C++ compiler comes with a decent implementation of the string class, as
well as container classes like set, list, and vector, I have chosen to use those im-
plementations in this gem rather than build custom, game-specific containers. If
you have special needs, such as fitting into a tight console memory space with a
fixed-address memory allocator, adapting the implementation to other string and
container classes is quite possible—read the implementation in
introspec-
tion.h
for more details.
To use the code as is in your own game, it’s easiest to copy the files
intro-
spection.cpp
, introspection.h, not_win32.h, and protocol.cpp into your
own project where they can be easily found. A more structured approach would
be to build a library out of the
.cpp files and set the include files in the project
25.3TheGem 403
settings (or the makefile) to reference the parent of the introspection directory.
Note that include files in the samples are included using angle brackets, naming
the introspection directory
<introspection/introspection.h>.
Given the above declaration, you can now do a number of interesting things
to any instance of
struct UserInfo. Most importantly, you can get a list of the
members of the structure, as well as their type and offset within the structure,
programmatically. The function
test_introspection() in the sample
main.cpp file shows how to do this and is illustrated in Listing 25.4.
void test_introspection()
{
std::stringstream ss;
const type_info_base& tib = UserInfo::member_info();
for (member_t::iterator ptr(tib.begin()), end(tib.end());
ptr != end; ++ptr)
{
ss << "member: " << (*ptr).name() <<
" desc: " << (*ptr).info().desc() <<
" offset: " << (*ptr).access().offset() <<
" size: " << (*ptr).access().size();
if ((*ptr).access().compound())
{
ss << " [compound]";
}
if ((*ptr).access().collection())
{
ss << " {collection}";
}
ss << std::endl;
std::cout << ss.str();
ss.str("");
}
}
Listing 25.4. Implementation of the test_introspection() function.
404 25.IntrospectionforC++GameEngines
The name of the type that contains information about each compound type is
type_info_base. It contains standard-template-library-style iterator accessors
begin() and end() to iterate over the members of the type, each of which is de-
scribed using a
member_t instance. Additionally, type_info_base contains an
access() accessor for the member_access_base type, which implements opera-
tions on the compound type itself, such as serializing it to and from a binary
stream, converting it to and from a text representation, and creating and destroy-
ing instances in raw memory. Each type (including the basic types like
int and
float) has a corresponding member_access_base, so this structure tells you
whether a type is compound (such as
struct UserInfo) and whether it is a col-
lection (such as a list or a vector).
25.4LiftingtheVeil
Examining every line of the implementation of the gem in detail is beyond the
scope of this chapter, but there are a few important things that deserve particular
attention.
Syntactically, the simple
INTROSPECTION(name, member member) mac-
ro takes only two arguments. Instead of listing each member using a comma,
each member is listed as a separate
MEMBER() macro, which in turn expands to
contain the comma if the implementation requires it. The current implementation
expands the
INTROSPECTION() macro to a static inline member function that re-
turns a static local instance of the member description type, and it initializes that
instance using the expansion of the member macros. The implementation of the
macros is shown in Listing 25.5.
#define INTROSPECTION(type, members)
typedef type self_t;
static inline const introspection::type_info_base& member_info()
{
static introspection::member_t data[] =
{
members
};
static introspection::type_info_t<type> info(
data, sizeof(data) / sizeof(data[0]),
introspection::struct_access_t<type>::instance());
25.5ToandFromaNetwork 405
return (info);
}
#define MEMBER(name, desc)
introspection::member_instance(&self_t::name, #name, desc),
Listing 25.5. The INTROSPECTION macro implementation.
This keeps the namespace of the introspected type (such as UserInfo) clean,
only introducing the
typedef self_t and the member_info() member function.
Access to each member of the structure is done using pointer-to-member syntax.
However, the actual pointer to member does not need to be dereferenced because
template metaprogramming can pick it apart and turn it into an offset-and-
typecast construct, to save run-time CPU cycles.
25.5ToandFromaNetwork
When you receive a piece of data from a network, you need to instantiate the
right data type, based on what the received packet is. Once the data type is dis-
patched, you have to correctly destroy the instance again, freeing up any addi-
tional memory used by members of types such as string or list. The introspection
mechanism of this gem builds this into the
member_access_base type, giving
you the
size() function to let you know how much memory to allocate, and
create() and destroy() functions to manage the lifetime of the structure in-
stance.
Additionally, the gem provides the
PROTOCOL() and PDU() macros to allow
definition of a suite of related data types. Each data type added with the
PDU()
macro is given a small integer ID, which you can use on the network to identify
which kind of packet is being sent. Given the
PROTOCOL instance, you can map a
type to an ID and an ID to a type using the
protocol_t type members code()
(to get the integer code for a type) and
type() (to get the type_info_base for a
code).
On top of this, you can build a type-safe protocol packet dispatcher, where
each dispatcher is selected by the appropriate type. End to end, the networking
API then uses the actual structure type to select which code to prefix the packet
with and serialize the structure to a sequence of bytes using the serialization from
the
INTROSPECTION() macro. It then sends this data to the other end, prefixed by
the size of the packet. The receiving end first decodes the integer of the packet
type and then selects the appropriate unpacker type instance based on the code.
406 25.IntrospectionforC++GameEngines
Using the
size() and create() functions, it inflates an instance of the appropri-
ate structure into an allocated array of bytes and constructs it using the appropri-
ate C++ constructor, after which it passes the instance through a dispatcher
selected by the integer code (type) of the packet. When the dispatcher returns, the
destroy() function calls the appropriate C++ destructor, and the memory can be
returned to the system as an array of bytes again.
This is illustrated in more detail in the
client and server programs, where
it is implemented in the
protocol_t::encode() and protocol_t::de-
code()
functions, respectively, using the memory stream functions of the sim-
ple_stream
class. A good example use of this mechanism is the send_-
a_message()
function from the client.cpp file, as shown in Listing 25.6.
static bool send_a_message(const char *line)
{
SaySomethingPacket ssp;
simple_stream ss;
ssp.message = line;
// Make space for the frame size field (short).
ss.write_bytes(2, "");
my_proto.encode(ssp, ss);
size_t plen = ss.position() - 2;
unsigned char *pdata = (unsigned char *) ss.unsafe_data();
// Generate a big-endian short for number of bytes count.
pdata[0] = (plen >> 8) & 0xFF;
pdata[1] = plen & 0xFF;
int l = send(sockfd, (const char *) pdata, plen + 2, 0);
if (l < 0)
{
// WSAGetLastError() may have been cleared by the other thread.
fprintf(stderr, "send error: %d ", WSAGetLastError());
}
return (l == plen + 2);
}
Listing 25.6. Sending a C++ data structure using automatic marshaling.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.227.9