Jump to content
  • Advertisement
Sign in to follow this  
GenuineXP

Help Converting from C++ to C Idioms (Streams)

This topic is 3797 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I'm at a loss as to how I can move from C++ idioms (streams) to C idioms. I need to do this with an interface used in a plugin system. I'm using the Boost.Iostreams library. The plugin in question is for the imaging system of my engine. It loads images, manipulates them, and typically transforms them into textures. Unlike textures, however, this is all done in system memory, dimensions don't need to be constrained, etc. (textures are essentially immutable). The problem I'm having is with loading functions. As in example, here's what loading from a file looks like (I've figured this one out well enough; it's pretty easy).
class image {
public:
    void load(const boost::filesystem::path& file) {
        do_load(reinterpret_cast<const int8_t*>(file.string().c_str()));
    }
    void load(boost::iostreams::stream<boost::iostreams::array_source>& in) {
        ...
    }
private:
    virtual void do_load(const int8_t* path) = 0;
};
This isn't a full listing, but it illustrates the point. image contains a non-virtual interface to virtual functions (a common idiom). The virtual functions that actually do the work (i.e., those prefixed with "do_") must use a very C interface to help maintain the compatibility of plugins in binary form, as well as to help support C based plugins. This is why do_load in the above example takes an array of characters (the full path to the file) instead of a path object. It's the other load function (the one taking a stream parameter) that I can't even figure out where to begin with. First of all, how can I represent a stream object in a C way? I thought of just passing a void pointer and a size parameter as the buffer the stream is reading from... but that buffer won't contain all of the data at any one time. I thought maybe a set of begin/end functions that move data in increments (sorta like a stream buffer) would work, but I'm not sure how to implement this. Any ideas? I also have versions of load that use streams based on file devices. The virtual "do_" functions cannot accept objects (just simple C structs at most) and should conform as much as possible to straight C. Thanks! I really can't seem to figure this one out.

Share this post


Link to post
Share on other sites
Advertisement
Given that C doesn't have virtual functions, you're basically SOL already. For full binary compatibility you're going to want to drop down entirely to the C ABI with the likes of extern "C" and function pointers. Otherwise, everything will be name mangled and you're already any binary compatibility between plugins built with one compiler and a base application built with so much as another version of the same compiler will be gone.

There's a few C "stream" idioms that come to mind to keep in mind:

1) FILE*s. These can't sanely be hooked into directly, so these warrant either a separate codepath or an adapter to your chosen one.

2) int handles and various typedefs thereof. Again, no hooking AFAIK.

3) "RWops". This is basically a structure of functions pointers, an idiom used for example by the SDL to help insulate code from multiple types of streamy objects. Adapting other handles to such an interface shouldn't be problematic, nor should wrapping these in C++ style streams (or, conversely, having them wrap C++ style streams) should be much of a problem. It sounds like you were already beginning to think toward this direction in your original post.

You'd probably end up with something like:

 // C image interface . h
#ifdef __cplusplus
#define EXTERN extern "C" __declspec(dllimport)
#else
#define EXTERN
#endif

typedef struct {} *IMAGE;
EXTERN IMAGE MyLib_Image_Create( void );
EXTERN void MyLib_Image_Load( IMAGE image, const char* path );




 // Implementation of the C interface in C++ . cpp
#define EXPORT "extern "C" __declspec(dllexport)

EXPORT void MyLib_Image_Create( void ) {
mylib::image* i = new mylib::image();
return (IMAGE)(void*)i;
}
EXPORT void MyLib_Image_Load( IMAGE image, const char* path ) {
((mylib::image*)(void*)image)->load( path );
// Note: This doesn't even need to directly interact with the virtual functions!
}




Or were you planning to try and allow your plugins to derive from the image class?

Share this post


Link to post
Share on other sites
Thanks for the reply.

All of the plumbing is already in place. Clients derive from the image class, or alternatively include other C headers that allow them to implement a plugin entirely in C. This also comes with handy adapters to wrap C "objects" and make language agnostic plugins (i.e., a plugin written in C++ that communicates entirely in C across binaries). Part of making this system possible and retaining the better compatibility of the C ABI is making sure these virtual functions use a C-like interface.

For FILE handles, I could just write custom Boost.Iostream objects that wrap said handles. This is a bit unnecessary though, because of already have a file system based loading function (I do have a Boost.Iostreams file_source based function as well... but I may as well drop that).

Share this post


Link to post
Share on other sites
Quote:
Original post by GenuineXP
Part of making this system possible and retaining the better compatibility of the C ABI is making sure these virtual functions use a C-like interface.


Still clear as mud for me. "Virtual functions" and "C-like" don't belong in the same sentence, especially in the context of ABI compatability.

If you're just trying to access C++ style streams from C code, you wouldn't even really need the RWops pattern, just hide it behind an opaque type and write some accessors as free functions:

// .h common defs
struct ostream_; typedef ostream_ *OSTREAM;
struct istream_; typedef istream_ *ISTREAM;

EXTERN int IS_Read ( ISTREAM is, char* buffer, size_t n );
EXTERN int OS_Write( OSTREAM os, const char* buffer, size_t n );

// If we want support for read/write streams:
struct iostream_; typedef iostream_ *IOSTREAM;
EXTERN OSTREAM OS( IOSTREAM ios );
EXTERN ISTREAM IS( IOSTREAM ios );
inline int IOS_Read ( IOSTREAM ios, char* buffer, size_t n ) { return IS_Read(IS(ios),buffer,n); }
inline int IOS_Write( IOSTREAM ios, const char* buffer, size_t n ) { return OS_Write(OS(ios),buffer,n); }




// .cpp implementation
EXPORT int IS_Read( ISTREAM is_, char* buffer, size_t n ) {
std::istream& is = *(std::istream*)(void*)is_;
if (!is.read(buffer,n)) return -1; // error
else return 0;
}
EXPORT int OS_Write( OSTREAM os_, const char* buffer, size_t n ) {
std::ostream& os = *(std::ostream*)(void*)os_;
if (!os.write(buffer,n)) return -1; // error
else return 0;
}

EXPORT OSTREAM OS( IOSTREAM ios ) { return (OSTREAM)(void*)(std::ostream*)(std::iostream*)(void*)ios; }
EXPORT ISTREAM IS( IOSTREAM ios ) { return (ISTREAM)(void*)(std::istream*)(std::iostream*)(void*)ios; }




// .hpp utilities

inline OSTREAM ToOS( std::ostream& os ) { return (OSTREAM)(void*)&os; }
inline ISTREAM ToIS( std::istream& is ) { return (ISTREAM)(void*)&is; }
inline IOSTREAM ToIOS( std::iostream& ios ) { return (IOSTREAM)(void*)&ios; }




Basically, just a mixture of opaque types instead of straight void* pointers to add a little type safety, and then the accessors that operate on those streams.

Share this post


Link to post
Share on other sites
Quote:
Original post by MaulingMonkey
Still clear as mud for me. "Virtual functions" and "C-like" don't belong in the same sentence, especially in the context of ABI compatability.

I haven't read the code in your post yet, but I see what you mean. The reason I need to use C idioms, even in virtual functions (clearly not C at all) is to avoid passing C++ objects to and from functions. It's possible that these virtual function calls will ultimately result in C function calls made across binaries. If I have a virtual function like this...

class foo {
public:
bool bar(const baz& b) {
return do_bar(b);
}
protected:
virtual bool do_bar(const baz& b) = 0;
};

...then I'll probably encounter problems when I need to pass a baz object across binaries, not only because of binary compatibility problems, but also because plugins may be written in pure C (and C code doesn't understand what a baz is... unless it's just a struct, of course).

Typically, the public non-virtual functions perform "conversions" and call the virtual work functions. They then convert any results before returning them. This way, clients just deal with the public interface, which is C++. So, really, it's returned values and parameters that must be C. Sorry; I really didn't make this clear.

I'll give the code a look over. Thanks for taking the time to write it. I appreciate it.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!