FMOD API

Started by
11 comments, last by bpaterson 10 years, 9 months ago

Hiya

I've recently taken a look at the FMOD API and I've noticed that it appears to be a C API, with a really thin C++ wrapper. I'm interested in understanding how they've implemented it.

The C++ classes in the header have no virtual methods or destructor, so they're not inheriting from these classes. Which makes me think that a member function like the following is implemented by just calling the equivalent C function:


FMOD_RESULT System::createSound(...)
{
  return FMOD_System_CreateSound(this, ...);
}

That C function takes a pointer to an FMOD_SYSTEM, which is declared like this:


typedef struct FMOD_SYSTEM FMOD_SYSTEM;

Which makes no sense to me. What does typedef'ing like this do, when FMOD_SYSTEM is undeclared? And how can the C function take an FMOD_SYSTEM parameter, but they must be passing a pointer to an FMOD::System class?

Many thanks!

Advertisement

What do the C++ headers look like? I don't have FMOD, but with more info about the header I could probably tell you what's going on.

You might be right about it being written or C, but it might be the other way around. They may have made a C-like C++ implementation to make it easier to put a C wrapper around their C++ code.

The typedef lets them avoid typing "struct FMOD_SYSTEM" everywhere. In C you cannot leave off struct in the type name unless you do a typedef. Without struct FMOD_SYSTEM defined, it's an incomplete type. Anything without the definition of what's inside the struct can only refer to it with a pointer. It's the C version of encapsulation.

I've been through adding a C++ library to a C program, and incomplete types are very useful in this situation. The C header may have its functions implemented in a C++ file, and that incomplete C struct could contain a C++ object inside, wherever it is actually defined. Or, they might never define it, and just force cast the pointer to whatever they use internally.

Are you using the official FMOD SDK? The Windows version contains .c and .cpp files for both C and C++ respectively which are both in the same folder for all the individual project files. In the SDK version that I have there is only C code in the .c file and the headers included both have .h extensions.

In the .cpp file both of the headers have .hpp extensions.

I double checked just now to confirm this, for me there is no mixing and matching between either formats. It's either C or C++, not both.

Incidentally, if you are using MingW/GCC you will have to use the C code. The library has a bug that causes what appear to be linker errors if you try to build the C++ version using the aforementioned compiler. That'll really drive you batty since the errors are completely unrelated to the problem.

Consider it pure joy, my brothers and sisters, whenever you face trials of many kinds, 3 because you know that the testing of your faith produces perseverance. 4 Let perseverance finish its work so that you may be mature and complete, not lacking anything.

Thanks for the replies!

I've been through adding a C++ library to a C program, and incomplete types are very useful in this situation. The C header may have its functions implemented in a C++ file, and that incomplete C struct could contain a C++ object inside, wherever it is actually defined. Or, they might never define it, and just force cast the pointer to whatever they use internally.

Yep - that seems to be what they're doing. I can't tell whether they use a C++ object internally or it really is implemented in C. I guess they could have done this for compatibility reasons. They actually define the C++ factory function to call the C function below. Although FMOD_SYSTEM is an incomplete struct type.


inline FMOD_RESULT System_Create(System **system) { return FMOD_System_Create((FMOD_SYSTEM **)system); }

Are you using the official FMOD SDK? The Windows version contains .c and .cpp files for both C and C++ respectively which are both in the same folder for all the individual project files. In the SDK version that I have there is only C code in the .c file and the headers included both have .h extensions.

I have the public SDK, which doesn't include the source code.

Is there any advantage to implementing a library like this using a C API, and then adding non-virtual C++ wrappers? I can't imagine many people want to use the C API, although I could be wrong. Possibly performance?

Thanks!


Although FMOD_SYSTEM is an incomplete struct type.


It's an opaque struct. These can be declared and used as pointers because the size of the struct need not be known at compile time. It's a way of hiding the implementation of the struct. It's a common C idiom. In C++, these are called forward references.


// foo.h
typedef struct foo_s foo_t;
 
foo_t* make_foo( void ); // this is ok
foo_t make_foo2( void ); // this will fail to compile

// foo.c
// implementation is not part of the public API and is hidden from the user.
struct foo_s {
   int bar;
   float baz;
};
 
foo_t* make_foo( void ) {
    foo_t *foo = malloc( sizeof foo_t );
    foo->bar = 10;
    foo->baz = 2.0f;
    return foo;
}

Is there any advantage to implementing a library like this using a C API, and then adding non-virtual C++ wrappers? I can't imagine many people want to use the C API, although I could be wrong. Possibly performance?


If you want a library to be linked (even dynamically) with anything generated by different compilers, there is really no way around a pure C API.
Even something simple as name mangling in C++ is not defined by the standard. In general two different different compilers will have different ways to encode the names and overloads of functions, so one compiler's linker will generally be unable to find the implementation of a function compiled by a different compiler simply because the name is not as expected. Then there are various ways the ABI can be different. Even between different versions of the same compiler the ABI can break.

If you want C++ code linked, you really only have two options:
(1) offer it precompiled for all compilers your users are likely to use. Note that you will probably need more than one build per compiler. For example for MSVC you will often want at least two (Debug and Release build with dynamic runtime), possibly four (Debug and Release build with static runtime) and possibly more depending on specific compiler options. Also note that a static link will generally require an exact match of practically all parameters.
(2) offer the whole source so the user can compile it exactly the way he needs it.

Since FMOD is closed source and they don't want to prebuild tons of different versions for all kinds of compilers, a pure C API is really the only way to go. A thin C++ wrapper which does nothing more than map to the C API is not a big deal. It can be present in source without showing the actually important code and compiling the wrapper will be trivial (if it is not header-only to start with).

Note that option (1) above is far from simple. It places rather stringent restrictions on the runtimes involved, the parameter types you are allowed to pass to functions and how much care you have to take about where something is allocated and deleted (as one restrictions is tightened others can generally be relaxed).
The C++ classes in the header

I'm so confused by this. I don't see a single class in the fmod.h file???

I see lots of C++ syntax in the comments but nothing in the actual code.

To my unsophisticated eye it looks like they just used their C++ comments in both the C and C++ headers to save the trouble of generating two sets of reference.

That's what you get for learning both C and C++ side by side. Aside from classes and a few other little details, I can't seem to tell which is which. oh well. rolleyes.gif

Consider it pure joy, my brothers and sisters, whenever you face trials of many kinds, 3 because you know that the testing of your faith produces perseverance. 4 Let perseverance finish its work so that you may be mature and complete, not lacking anything.

Brett here from FMOD.

FMOD is written in C++ not C! :)

Here is the code we use. The C++ interface is the native interface, and the C header is actually a wrapper for the C++ interface. FMOD_SYSTEM is an opaque type that can be used interchangeably with the FMOD::System type. Its correct that having the C interface allows the DLL to export STDCALL symbols that any language can hook into. This is how we supported mingw/gcc/delphi/visual basic/C# etc.


FMOD_RESULT F_API FMOD_System_CreateSound(FMOD_SYSTEM *system, const char *name_or_data, FMOD_MODE mode, FMOD_CREATESOUNDEXINFO *exinfo, FMOD_SOUND **sound)
{
    FMOD::System *_system = (FMOD::System *)system;

    return _system->createSound(name_or_data, mode, exinfo, (FMOD::Sound **)sound);
}

FMOD_RESULT System::createSound(const char *name_or_data, FMOD_MODE mode, FMOD_CREATESOUNDEXINFO *exinfo, Sound **sound)
{
    FMOD_RESULT result;
    SystemI *systemi;


    result = SystemI::validate(this, &systemi);
    if (result != FMOD_OK)
    {
        return result;
    }
    else
    {
        return systemi->createSound(name_or_data, mode, exinfo, (SoundI **)sound);
    }
}

FMOD_RESULT SystemI::validate(System *system, SystemI **systemi)
{
    FMOD::SystemI *sys = (FMOD::SystemI *)system;
    
    if (!system || !systemi)
    {
        return FMOD_ERR_INVALID_PARAM;
    }


    if (!FMOD::gGlobal->gSystemHead->exists(&sys->mNode))
    {
        return FMOD_ERR_INVALID_HANDLE;
    }
    
    *systemi = sys;


    return FMOD_OK;
}

another trick we do is use reference counted integers for FMOD::Channel/FMOD_CHANNELs. People declare them as FMOD::Channel *, but they're not actually pointers to memory.

This allows us to error check the handle, and reference count it, so if it gets stolen by another channel, it will become invalid and channel functions will start returning FMOD_ERR_CHANNEL_STOLEN errors .. Like above , the ChannelI::validate function takes the base type in (the integer), and gives an implementation object out (FMOD::ChannelI). The validate function dissasembles the handle bitfield and looks up a table to get the correct FMOD::ChannelI object for internal use.

We have had support emails in the past about why our pointers look so strange, so we had to juggle some bits around so it looked more 'pointery'.

cool. You wouldn't happen to be Brett Paterson, CEO, Firelight Technologies, would you?

Consider it pure joy, my brothers and sisters, whenever you face trials of many kinds, 3 because you know that the testing of your faith produces perseverance. 4 Let perseverance finish its work so that you may be mature and complete, not lacking anything.

This topic is closed to new replies.

Advertisement