While there are a lot of Open Source C libraries, there are no particularly strong conventions for distributing them.
In the GNU and Linux ecosystems, it is reasonably common to use Autotools as a build system, so that users can download your source code and then run
./configure
make
sudo make install
to build and install the library.
But many developers do not like globally installed libraries, and would instead vendor your project (copying your files into their source tree) or use a Git-Submodule.
To make this simple, it is typically best if your projects do not have further dependencies, or at least make those dependencies very clear so that other developers can install them.
This ties into your question #4: If you can split thematically distinct parts into different projects, that's probably a good idea. This is the library-level equivalent to the OOP “interface segregation principle”, that users of your libraries should not be forced to depend on functionality they don't need. But since dependency management is such a pain for C projects, combining different parts into a monorepo is often the lesser evil.
Regarding #1, there are no strong conventions for project layouts. You will see everything from
- a bunch of
.c
and .h
files in the top level of a project, without any directories
- a separate
include
directory for public headers (that dependent projects should add to their include path)
- a
src
directory for the .c files and internal headers
- if the repository contains multiple distinct libraries, separate directories for those libraries
If you do anything unusual, consider writing an architecture guide to help others navigate the code base.
Regarding #2, there are no special concerns for Open Source vs other code when it comes to introducing data types. As with all C projects, it makes sense to give those type names a project-specific prefix to prevent name clashes. As you evolve the layout of types, it is good to keep API- and ABI-compatibility in mind.
If your library is intended for static linking or is header-only, ABI compatibility doesn't matter. However, developers will appreciate API-stability so that they can upgrade to a new version of your library without having to update their code. For example, adding a field to a struct which is defined in a header file will break API compatibility, unless you provide a function or macro to initialize a default value for that struct.
Libraries intended for dynamic linking should be clear about their ABI compatibility guarantees, so that dependent code can use new versions of your library without needing a full recompilation. Anything that affects function signatures or struct layout will likely break binary compatibility. But there are well-known strategies to help here, such as using incomplete types in your header files so that dependent code can only handle pointers to such types.
Regarding #3, there's generally the expectation that one .c file is one compilation unit. If you want to combine a lot of code into a single compilation unit for linkage reasons (i.e. so that all the code has access to some static
functions, enabling stronger optimizations), then I'd use the preprocessor to include those code fragments into a single .c file. Note that passing multiple .c files to a single compiler invocation (gcc -o foo file1.c file2.c
) is just a shorthand for compiling and linking those files separately. It would be unusual to see this shorthand in build scripts.