#65 Enable SoX to be built as a Windows DLL

Doug Cook

Two primary things have to be done to enable building as a DLL.

1. Properly annotate anything that needs to be exported (published) from the DLL. This can be done either by creating a sox.DEF file or by annotating the sox.h header file, but it's better to annotate the header file. For one thing, you can't correctly export data (global variables) via a DEF file, and SoX is exporting several global variables (for now, anyway). Functions can be exported either way (or both ways). For another thing, there is a slight performance penalty if the compiler doesn't know that the function it's calling comes from a DLL, so you get a tiny performance boost if you annotate the header file so that the compiler knows what to expect.

2. Ensure that the DLL exports a consistent view of itself and does not cause problems or limitations for the consumer. For example, the set of functions exported by the DLL should not change based on the features compiled into the DLL (or if they do change, they should change in a well-defined manner). So whether or not HAVE_STRCASECMP is defined when the DLL is built, the DLL needs to export the same set of symbols. But the DLL must not define symbols that are likely to conflict with other DLLs or libraries, so the DLL must not export a symbol named strcasecmp since that is likely to conflict with existing definitions.

After applying this patch, I can build libsox.dll (all source files except sox.c), and then build a sox.exe that links against libsox.dll and runs correctly. There are still a few loose ends:

1. sox.h still uses the type FILE the definition of the sox_format structure. If the DLL and the client are built using different C Runtime Libraries (i.e. MSVC9 and MinGW), or if the DLL and the client are each statically-linked with their C Runtime Libraries, they will not be able to share FILE objects (each CRT has its own table of valid file ids and the FILE objects from one CRT will not work correctly if passed to file APIs of a different CRT). This is probably not a problem as long as the client doesn't try to actually touch it.
2. SoX still makes use of global variables. In theory, a DLL should be able to serve multiple clients within the same process, and each client should get its own settings, independent of the settings of any other client (perhaps the client is running two audio processing threads at once). However, in practice, this doesn't seem to be a pressing issue or a major requirement, and we can probably safely postpone it.

On to the specific changes...

effects.c, formats.c, libsox.c:
- Importing data from DLLs is tricky, and some clients can't do it at all. For each global data member that the libsox DLL needs to expose, provide a corresponding function that returns a pointer to the same data. This is a messy solution, but it works for now.

- As previously defined, sox_output_message could not be exported from a DLL, since its FILE* parameter might not be compatible with the client's CRT, which could lead to all kinds of trouble. So I changed it to take the same parameters as a real output handler. example3.c needed to be updated accordingly.

- Combine sox_output_message (a helper method for creating output handlers) and output_message (the default output handler). We can't safely export sox_output_message (it takes a FILE* parameter), so it can't be used as a helper. But we can export the default output handler, and if we flush the output stream after each use (in the case of two different CRTs, each stderr stream will have its own buffering), it be used safely by the user as a part of a more sophisticated output handler.

- If memory is allocated via one heap allocation system, it needs to be deallocated using the same heap. Crossing the DLL boundary means that the DLL and the EXE might have different versions of malloc and free. Since sox.c is using the DLL's malloc to allocate memory (via the exported lsx_realloc), it MUST use the DLL's free (via the exported lsx_realloc).
- Adjust to the new definition of sox_output_message.

- Define a macro for the annotation of symbols. The three cases are that the symbol might need to be exported from the DLL (when the header is used by components of the DLL), imported from the DLL (when the header is used by components of the client), or neither (when not building a DLL, when the client is statically linking with SoX, or when not using Windows). The client should define SOX_IMPORT to indicate that the client is building against a DLL version of SoX instead of statically linking. When SoX is being built as a DLL, this is detected by the presence of _DLL which is added to the project by Visual Studio when building a DLL project. If neither is set, assume static linkage between SoX and its client. If this isn't set up correctly, the symptoms to expect are as follows:
--- If the client doesn't set SOX_IMPORT, the client will crash when it tries to access any global variables from SoX. (If we eliminate global variables, then SOX_IMPORT will become optional and will just be a slight optimization.)
--- If _DLL is not set when building libsox.dll, the DLL will not export any symbols. (If we eliminate global variables, then this would also be optional because we could then just use a DEF file instead of annotating the header.)
- Added "extern C" around the declarations so that the name mangling is the same whether you are writing a C program that includes the sox.h header or a C++ program that includes the header. Not too important if statically linking, but very important if making a DLL.
- Remove redundant include of stddef, since it is not acceptable to include headers within an "extern C" region.
- Add SOX_EXPORT to all exported symbols.
- Add declarations for the new functions that return pointers to the global data objects.
- sox.c uses lsx_realloc, so libsox.dll needs to export it.
- sox.c sometimes (depending on the definition of HAVE_STRCASECMP when sox.c is compiled, not the defintion when the DLL is compiled) uses strncasecmp from util.c, so libsox.dll needs to export it. It can't use the original name because that might conflict with another library, so add a prefix and fix things up via #define.

- strcasecmp becomes a macro that is defined only if we don't HAVE_STRCASECMP, so we no longer declare strcasecmp.
- strcasecmp is used by sox.c, which doesn't include sox_i.h, so the macro needs to be move elsewhere (function was ok being here, though it raised a warning in the compile of sox.c before; the new definition using a macro doesn't work at all in sox_i.h and needs to move to util.h).
- lsx_strcasecmp is now declared in sox.h and does not need to be declared in sox_i.h.

- We now need to always define lsx_strcasecmp, whether or not HAVE_STRCASECMP is defined, since we don't know at DLL compile time whether it will be defined at client compile time. However, we can implement it using the built-in function, if available.

- Define the macro for strcasecmp here so that sox.c can see it.

- lsx_realloc is now defined in sox.h because sox.c needs it.
- lsx_free is now defined so that memory allocated using lsx_*alloc can be freed using lsx_free.


  • Doug Cook

    Doug Cook - 2009-09-15

    Patch for enabling creation of libsox.dll

  • Doug Cook

    Doug Cook - 2009-09-16

    Looking through this, I wonder if it might not be a good idea to postpone this a little while. This changelist is sufficient to make a functional libsox DLL, but the API still has some areas that don't work well in the context of a shared library. It might be better work through those issues and make one one well-thought-out change instead of making a series of incompatible changes.


Log in to post a comment.