Subscribe to 10 GOTO 10        RSS Feed
***** 1 Votes

X-Macros in C: Abstracting Datafiles

Icon Leave Comment
Using X-Macros in C
case study: Abstracting data file usage

The following comes from a conversation I had a little while back: I was trying to help a student who came to me with an assignment that required several databases and needed to done ASAP. The code the student already had was just not anywhere near done and so I took on the challenge of finding a way to quickly get the assignment done. Well to try to help out I turned to abstraction -- specifically I turned to X-Macros to make the most use of as few function as possible. The idea was to take a few abstract functions and build an interface with several binary data files containing the various databases. I wanted to reduce what needed to be written by hand, but leave the student with a nice API for accessing the data.

The problem was an airline reservation system and the assignment (as I read it) required the following data tables: AirplaneData, RouteData, PassengerData, ManifestData, FlightData, and UserData. The teacher obviously did not want them to use something like SQLite but wanted to see them using file IO. Putting 1 table per-file should make things pretty simple and a basic binary file with record data would work fine. So the problem now is to write CRUD operations (Create, Read, Update, Delete a record) for all 6 data files... Well that would take forever. We could do everything abstract with casts to and from void pointers, but that can be so ugly and error-prone when hand coded and would take forever to write. I would not want to deal with this kind of interface for each operations -- so the challenge was to make something easy.

So remembering my design patterns I said, "well we can make some general abstract CRUD operations and then wrap this interface in a Facade for each database". If we created a set of general functions for dealing with data files, then we wrap each of these for each database type (using meta-programming to reduce chance for typos and errors). There are 6 basic operations: Load the data into memory, Create a record, Find a record by id, Update a record, Delete a record, and Save the database back to file. None of these operations are particularly dependent upon the actual data (except find by id, but even that can be generalized), all we deed to know is the SIZE of the record. . . . but we are still talking about a LOT of typing! (or at least intricate copy & paste)

Introduce meta-programming and by using X-Macros we can implement a generalized function wrap each function into a macro and then stamp out a copy specific to the database we need! Write one general, then customize and stamp out 6 different versions!

So each record type looked something like this:
typedef struct {
    int id;
    int numSeats;
    char name[20];
    int costPerMile;
} AirplaneData;


Note that we made each record type have an id field first in the structure even if we didn't think it would be used necessarily. The reason for this was so that we could write one findByID() function that would work for ALL of the databases. Again thinking completely generic, in general we don't want to have to write a function specific to any one database unless it is specific to solving a problem with that particular data.

Next we created an enumeration to keep track of the various kinds of databases and their files. This was probable not necessary but there was a method to the madness, it would be nice to have a way to ensure that each of the database files is only opened by the correct function.
typedef enum {
    FTYPE_NONE,
    FTYPE_AIRPLANE,
    FTYPE_ROUTE,
    FTYPE_PASSENGER,
    FTYPE_MANIFEST,
    FTYPE_FLIGHT,
    FTYPE_USER,
} FileTypes;


Now we create a special set of types to wrap a "database". The Generic version looks like:
typedef struct {
    const char* filename; // each database has a file name
    void *data; //The contents of the data in memory -- This pointer will be customized for each database
    unsigned int size; // the number of records
    unsigned int structSize; //the size of one record
    FileTypes type; // the type of database
} GenericDB;


We want to replace void* with a pointer to the actual record type so that there could be an array in memory with that data. A simple X-Macro then was used to create a copy of this for each database type:

First create a template macro:
#define DECLARE_DB_TYPE(DBtype, struct_type, file_type) \
    typedef struct { \
        const char* filename; \
        struct_type *data; \
        unsigned int size; \
        unsigned int structSize; \
        FileTypes type; \
    } DBtype;


Then an X-Macro and table:
#define X(DBtype, struct_type, file_type) DECLARE_DB_TYPE(DBtype, struct_type, file_type)
X(GenericDB, void, FTYPE_NONE)
X(AirplaneDB, AirplaneData, FTYPE_AIRPLANE)
X(RouteDB, RouteData, FTYPE_ROUTE)
X(PassengerDB, PassengerData, FTYPE_PASSENGER)
X(ManifestDB, ManifestData, FTYPE_MANIFEST)
X(FlightDB, FlightData, FTYPE_FLIGHT)
X(UserDB, UserData,  FTYPE_USER)
#undef X


Because the table will get used over and over I actually like to break it out into its own little macro that I call "X_TABLE" though sometimes people just put this into a separate file and just #include it. I find that I prefer to keep it all in the same file.

The result after the pre-processor finishes is this:
typedef struct {
    const char* filename;
    void *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} GenericDB;
typedef struct {
    const char* filename;
    AirplaneData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} AirplaneDB;
typedef struct {
    const char* filename;
    RouteData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} RouteDB;
typedef struct {
    const char* filename;
    PassengerData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} PassengerDB;
typedef struct {
    const char* filename;
    ManifestData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} ManifestDB;
typedef struct {
    const char* filename;
    FlightData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} FlightDB;
typedef struct {
    const char* filename;
    UserData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} UserDB;


The X-Macro has arguments that we did not use? The reason is that we want to be able to use the SAME table (or roughly the same, we don't use the GenericDB entry ever again) for each of the X-Macros. This allows us to use the same X-Table:
#define X_TABLE \
X(AirplaneDB, AirplaneData, FTYPE_AIRPLANE) \
X(RouteDB, RouteData, FTYPE_ROUTE) \
X(PassengerDB, PassengerData, FTYPE_PASSENGER) \
X(ManifestDB, ManifestData, FTYPE_MANIFEST) \
X(FlightDB, FlightData, FTYPE_FLIGHT) \
X(UserDB, UserData,  FTYPE_USER)

over and over again whenever we need to create something for each of the database types. To that end we have to ensure that our X-Macro has arguments appropriate to all of the ways it will get used. Here I generally need the Database type, the Record type, then the database type from the enumeration FileTypes.

Now to make create our template macro for a function that will load our databases into memory:
GenericDB db_loadDatabase(const char *filename, unsigned int struct_size, FileTypes type);
#define DECLARE_DB_LOADER(DBtype, struct_type, file_type) \
    DBtype Load ## DBtype(const char *filename) { \
        GenericDB db = db_loadDatabase(filename, sizeof(struct_type), file_type); \
        return *((DBtype*)(&db));  \
    }

So first we declare the generic function db_loadDatabase() which take a file name and will load data from a file into an in-memory array. The function will need to know the size of the records it will be loading and the type of file (to ensure the correct type of database is loaded). Once we have this function we can take the DECLARE_DB_LOADER() macro as a template for building new functions specific to each database type. Because GenericDB is the same as any other DB structure (except it has a void*) we can cast any DB from generic to another form -- thus returning the correct DB type.

So now we implement the X-Macro and table:
#define X(DBtype, struct_type, file_type) DECLARE_DB_LOADER(DBtype, struct_type, file_type)
X_TABLE
#undef X


The pre-processor finishes with that we get:
AirplaneDB LoadAirplaneDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(AirplaneData), FTYPE_AIRPLANE);
    return *((AirplaneDB*)(&db));
}
RouteDB LoadRouteDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(RouteData), FTYPE_ROUTE);
    return *((RouteDB*)(&db));
}
PassengerDB LoadPassengerDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(PassengerData), FTYPE_PASSENGER);
    return *((PassengerDB*)(&db));
}
ManifestDB LoadManifestDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(ManifestData), FTYPE_MANIFEST);
    return *((ManifestDB*)(&db));
}
FlightDB LoadFlightDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(FlightData), FTYPE_FLIGHT);
    return *((FlightDB*)(&db));
}
UserDB LoadUserDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(UserData), FTYPE_USER);
    return *((UserDB*)(&db));
}
Six functions for the price of one! By wrapping the generic db_loadDatabase function in this way we have reduced the work to writing one function that deals with a datafile of structures of a given size. The db_loadDatabase does not need to care about what kind of structure or what kind of data, all it does is read the data off of the disk and put it into memory. Also note that we have simplified our interface. Rather than needing some generic function db_loadDatabase() that needs to know the structure size etc. Our new interface functions hide all of the nasty casting and provide the user of the interface with a simple function taking just a file name as an argument!

The same thing can be done for the other functions. Our basic CRUD operations are all generic and can implemented in some generic "db_operation" function and then wrapped in an X-Macro. The end result is a basic standard interface for each database that can be easily used to create the application.

Well in the end I think that I just wasted valuable time for the student as I saw later that they tried to get the original code going and I am sorry that my approach was such a waste of time. But, it was a way to reduce programming to just a few functions and still provide a nice interface to the data. In the end at least I have an example of how X-Macros can drastically reduce the amount of work required.

Here was the full header that I started out for the student:
#ifndef DATABASE_HEADER
#define DATABASE_HEADER

typedef struct {
    int id;
    int numSeats;
    char name[20];
    int costPerMile;
} AirplaneData;

typedef struct {
    int id;
    int manifestId;
    char source[20];
    char destination[20];
} RouteData;

typedef struct {
    int id;
    char fname[20];
    char lname[20];
} PassengerData;

typedef struct {
    int id;
    int airplainId;
    int firstClass[60];
    int business[140];
    int economy[200];
} ManifestData;

typedef struct {
    int id;
    int ManifestID;
    int flightTime;
    int price_fc;
    int price_bus;
    int price_eco;
    long startTime;
} FlightData;

typedef enum {
    USER_NONE = 0x00,
    USER_AGENT = 0x01,
    USER_SUPERVISOR = 0x02,
    USER_ADMIN = 0x03,
} UserTypes;

typedef struct {
    int id;
    char username[20];
    char password[20];
    UserTypes role;
} UserData;

typedef enum {
    FTYPE_NONE,
    FTYPE_AIRPLANE,
    FTYPE_ROUTE,
    FTYPE_PASSENGER,
    FTYPE_MANIFEST,
    FTYPE_FLIGHT,
    FTYPE_USER,
} FileTypes;

typedef struct {
    FileTypes fileTypeID;
    int numItems;
} FileHeader;

#define X_TABLE \
X(AirplaneDB, AirplaneData, FTYPE_AIRPLANE) \
X(RouteDB, RouteData, FTYPE_ROUTE) \
X(PassengerDB, PassengerData, FTYPE_PASSENGER) \
X(ManifestDB, ManifestData, FTYPE_MANIFEST) \
X(FlightDB, FlightData, FTYPE_FLIGHT) \
X(UserDB, UserData,  FTYPE_USER)

#define DECLARE_DB_TYPE(DBtype, struct_type, file_type) \
    typedef struct { \
        const char* filename; \
        struct_type *data; \
        unsigned int size; \
        unsigned int structSize; \
        FileTypes type; \
    } DBtype;

#define X(DBtype, struct_type, file_type) DECLARE_DB_TYPE(DBtype, struct_type, file_type)
X(GenericDB, void, FTYPE_NONE)
X_TABLE
#undef X

#define REMOVED_ID -1

//--- Seat functions ---
int getNumFirstClass(int seats);
int getNumBusiness(int seats);
int getNumEconomy(int seats);

//--- DataBase IO Functions ---
GenericDB db_loadDatabase(const char *filename, unsigned int struct_size, FileTypes type);
#define DECLARE_DB_LOADER(DBtype, struct_type, file_type) \
    DBtype Load ## DBtype(const char *filename) { \
        GenericDB db = db_loadDatabase(filename, sizeof(struct_type), file_type); \
        return *((DBtype*)(&db));  \
    }

int db_saveDatabase(GenericDB *db);
#define DECLARE_DB_SAVER(DBtype, struct_type, file_type) \
    int save ## DBtype(DBtype *db) { \
        return db_saveDatabase((GenericDB*) db); \
    }

// Database generic CRUD (create, read, update, delete) operations
void db_addRecord(GenericDB *db, void* record);
#define DECLARE_DB_ADD(DBtype, struct_type, file_type) \
    void add ## DBtype(DBtype *db, struct_type record) { \
        db_addRecord((GenericDB*) db, &record); \
    }

void db_updateRecord(GenericDB *db, int id, void* record);
#define DECLARE_DB_UPDATE(DBtype, struct_type, file_type) \
    void update ## DBtype(DBtype *db, int id, struct_type record) { \
        db_updateRecord((GenericDB*) db, id, &record); \
    }

void db_deleteRecord(GenericDB *db, int id);
#define DECLARE_DB_DELETE(DBtype, struct_type, file_type) \
    void delete ## DBtype(DBtype *db, int id) { \
        db_deleteRecord((GenericDB*) db, id); \
    }

void* db_findByID(GenericDB *db, int id);
#define DECLARE_DB_FINDBYID(DBtype, struct_type, file_type) \
    struct_type findByID_ ## DBtype(DBtype *db, int id) { \
        struct_type* record = db_findByID((GenericDB*) db, id); \
        if (!record) { \
            struct_type temp; \
            temp.id = REMOVED_ID; \
            return temp; \
        } else { \
            return *record; \
        } \
    }

#define X(DBtype, struct_type, file_type) DECLARE_DB_LOADER(DBtype, struct_type, file_type)
X_TABLE
#undef X

#define X(DBtype, struct_type, file_type) DECLARE_DB_SAVER(DBtype, struct_type, file_type)
X_TABLE
#undef X

#define X(DBtype, struct_type, file_type) DECLARE_DB_ADD(DBtype, struct_type, file_type)
X_TABLE
#undef X

#define X(DBtype, struct_type, file_type) DECLARE_DB_UPDATE(DBtype, struct_type, file_type)
X_TABLE
#undef X

#define X(DBtype, struct_type, file_type) DECLARE_DB_DELETE(DBtype, struct_type, file_type)
X_TABLE
#undef X

#define X(DBtype, struct_type, file_type) DECLARE_DB_FINDBYID(DBtype, struct_type, file_type)
X_TABLE
#undef X

//Because of how X macros reuse Macro names, it is IMPORTANT to undefine macros when you are done with them.
//  These macros are NOT meant to be used as pseudo-functions in our code, these macros had a specific purpose
//  and now that we are done with them, we undefine them to ensure they don't get misused by accident.
#undef X_TABLE
#undef DECLARE_DB_TYPE
#undef DECLARE_DB_LOADER
#undef DECLARE_DB_SAVER
#undef DECLARE_DB_ADD
#undef DECLARE_DB_UPDATE
#undef DECLARE_DB_DELETE
#undef DECLARE_DB_FINDBYID

#endif


and this is what the preprocessor produces:
typedef struct {
    int id;
    int numSeats;
    char name[20];
    int costPerMile;
} AirplaneData;

typedef struct {
    int id;
    int manifestId;
    char source[20];
    char destination[20];
} RouteData;

typedef struct {
    int id;
    char fname[20];
    char lname[20];
} PassengerData;

typedef struct {
    int id;
    int airplainId;
    int firstClass[60];
    int business[140];
    int economy[200];
} ManifestData;

typedef struct {
    int id;
    int ManifestID;
    int flightTime;
    int price_fc;
    int price_bus;
    int price_eco;
    long startTime;
} FlightData;

typedef enum {
    USER_NONE = 0x00,
    USER_AGENT = 0x01,
    USER_SUPERVISOR = 0x02,
    USER_ADMIN = 0x03,
} UserTypes;

typedef struct {
    int id;
    char username[20];
    char password[20];
    UserTypes role;
} UserData;

typedef enum {
    FTYPE_NONE,
    FTYPE_AIRPLANE,
    FTYPE_ROUTE,
    FTYPE_PASSENGER,
    FTYPE_MANIFEST,
    FTYPE_FLIGHT,
    FTYPE_USER,
} FileTypes;

typedef struct {
    FileTypes fileTypeID;
    int numItems;
} FileHeader;

typedef struct {
    const char* filename;
    void *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} GenericDB;
typedef struct {
    const char* filename;
    AirplaneData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} AirplaneDB;
typedef struct {
    const char* filename;
    RouteData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} RouteDB;
typedef struct {
    const char* filename;
    PassengerData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} PassengerDB;
typedef struct {
    const char* filename;
    ManifestData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} ManifestDB;
typedef struct {
    const char* filename;
    FlightData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} FlightDB;
typedef struct {
    const char* filename;
    UserData *data;
    unsigned int size;
    unsigned int structSize;
    FileTypes type;
} UserDB;

int getNumFirstClass(int seats);
int getNumBusiness(int seats);
int getNumEconomy(int seats);

GenericDB db_loadDatabase(const char *filename, unsigned int struct_size, FileTypes type);

int db_saveDatabase(GenericDB *db);

void db_addRecord(GenericDB *db, void* record);

void db_updateRecord(GenericDB *db, int id, void* record);

void db_deleteRecord(GenericDB *db, int id);

void* db_findByID(GenericDB *db, int id);

AirplaneDB LoadAirplaneDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(AirplaneData), FTYPE_AIRPLANE);
    return *((AirplaneDB*)(&db));
}
RouteDB LoadRouteDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(RouteData), FTYPE_ROUTE);
    return *((RouteDB*)(&db));
}
PassengerDB LoadPassengerDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(PassengerData), FTYPE_PASSENGER);
    return *((PassengerDB*)(&db));
}
ManifestDB LoadManifestDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(ManifestData), FTYPE_MANIFEST);
    return *((ManifestDB*)(&db));
}
FlightDB LoadFlightDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(FlightData), FTYPE_FLIGHT);
    return *((FlightDB*)(&db));
}
UserDB LoadUserDB(const char *filename) {
    GenericDB db = db_loadDatabase(filename, sizeof(UserData), FTYPE_USER);
    return *((UserDB*)(&db));
}


int saveAirplaneDB(AirplaneDB *db) {
    return db_saveDatabase((GenericDB*) db);
}
int saveRouteDB(RouteDB *db) {
    return db_saveDatabase((GenericDB*) db);
}
int savePassengerDB(PassengerDB *db) {
    return db_saveDatabase((GenericDB*) db);
}
int saveManifestDB(ManifestDB *db) {
    return db_saveDatabase((GenericDB*) db);
}
int saveFlightDB(FlightDB *db) {
    return db_saveDatabase((GenericDB*) db);
}
int saveUserDB(UserDB *db) {
    return db_saveDatabase((GenericDB*) db);
}


void addAirplaneDB(AirplaneDB *db, AirplaneData record) {
    db_addRecord((GenericDB*) db, &record);
}
void addRouteDB(RouteDB *db, RouteData record) {
    db_addRecord((GenericDB*) db, &record);
}
void addPassengerDB(PassengerDB *db, PassengerData record) {
    db_addRecord((GenericDB*) db, &record);
}
void addManifestDB(ManifestDB *db, ManifestData record) {
    db_addRecord((GenericDB*) db, &record);
}
void addFlightDB(FlightDB *db, FlightData record) {
    db_addRecord((GenericDB*) db, &record);
}
void addUserDB(UserDB *db, UserData record) {
    db_addRecord((GenericDB*) db, &record);
}


void updateAirplaneDB(AirplaneDB *db, int id, AirplaneData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}
void updateRouteDB(RouteDB *db, int id, RouteData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}
void updatePassengerDB(PassengerDB *db, int id, PassengerData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}
void updateManifestDB(ManifestDB *db, int id, ManifestData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}
void updateFlightDB(FlightDB *db, int id, FlightData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}
void updateUserDB(UserDB *db, int id, UserData record) {
    db_updateRecord((GenericDB*) db, id, &record);
}


void deleteAirplaneDB(AirplaneDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}
void deleteRouteDB(RouteDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}
void deletePassengerDB(PassengerDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}
void deleteManifestDB(ManifestDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}
void deleteFlightDB(FlightDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}
void deleteUserDB(UserDB *db, int id) {
    db_deleteRecord((GenericDB*) db, id);
}


AirplaneData findByID_AirplaneDB(AirplaneDB *db, int id) {
    AirplaneData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        AirplaneData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}
RouteData findByID_RouteDB(RouteDB *db, int id) {
    RouteData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        RouteData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}
PassengerData findByID_PassengerDB(PassengerDB *db, int id) {
    PassengerData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        PassengerData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}
ManifestData findByID_ManifestDB(ManifestDB *db, int id) {
    ManifestData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        ManifestData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}
FlightData findByID_FlightDB(FlightDB *db, int id) {
    FlightData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        FlightData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}
UserData findByID_UserDB(UserDB *db, int id) {
    UserData* record = db_findByID((GenericDB*) db, id);
    if (!record) {
        UserData temp;
        temp.id = -1;
        return temp;
    } else {
        return *record;
    }
}


X-Macros may not be the most readable construct in programming, and I really have not found them too useful for C++ where templates are often a better solution BUT they DO have some advantages over templatates! You can run the preprocessor on the X-Macros and then use the result against the compiler to have much much more successful troubleshooting! Where templates can be very difficult to troubleshoot compiler problems, X-Macros are much much easier. :)


For more reading on X-Macros:
Advanced Preprocessor topic in C++ Programmers - I show a basic usage for generating string/enumeration sets.
Wikipedia X-Macros - A basic introduction.
Code generation with X-Macros in C -- by ben - talks about using them to extract information from an ini.
Dr Dobbs: The New C: X Macros -- by Randy Meyers (may 2001) - I think this is probably where I picked up the idea, demonstrates generating enum/array sets.
On Programming: X-Macros -- An intro from someone who obviously has some experience using X-Macros of different forms.
The Shorted Turn: Increase Your Code Power: X-Macros -- by Gary R. Van Sickle - another Enum/Array synchronization
C++ Map with multiple data types? Post #11 -- Here i used X-Macros in the generation of a PolyType variant type class. Demonstrates how X-Macros can be used for switch-case conditions -- when mixed with enums this is similar to the enum/array combination.

0 Comments On This Entry

 

December 2014

S M T W T F S
 123456
78910111213
1415161718 19 20
21222324252627
28293031   

Recent Entries

Search My Blog

0 user(s) viewing

0 Guests
0 member(s)
0 anonymous member(s)