Feature System Guide
What Are Features?
Features are a way to enable or disable optional parts of a library or your own project. They are useful for:
- Conditional Compilation: Compile only the code you need, reducing binary size
- Optional Dependencies: Include dependencies only when their associated features are used
- Customization: Allow users of your library to choose which functionality to include
- Testing: Verify different configurations of your code
- Platform-specific Code: Enable features based on target platform
Real-World Analogy
Think of building a car. You might have these options:
- Sport Package: adds spoiler, upgraded suspension, larger engine
- Sunroof: adds sliding glass roof and motor
- Leather Seats: premium interior
- Advanced Safety: collision detection, blind spot monitoring
You don’t need all features for every customer. The feature system lets you “configure” your library (or the libraries you depend on) to include exactly what you need.
Why This Matters for C
In C, conditional compilation is traditionally done with:
#ifdef ENABLE_LOGGING
printf("debug info\n");
#endif
But managing these flags manually is error-prone. Coffee’s feature system automates:
- Defining which features exist
- Which
-Dcompiler flags to pass for each feature - Which optional dependencies are needed
- Passing
-Dflags to the compiler automatically
Defining Features in Coffee.toml
Features are defined in the [features] section of Coffee.toml. For the complete syntax and examples, see the Features section in the Coffee.toml specification.
Default Features
The default feature is enabled automatically unless --no-default-features is passed:
coffee build --no-default-features
Optional Dependencies
Mark a dependency as optional = true in [dependencies], then reference it in a feature. If the feature is not enabled, the dependency is not linked.
See the Optional Dependencies example in the Coffee.toml specification.
Using Features in Your Code
In Source Files
Use preprocessor conditionals:
#include "mylib.h"
void do_something(bool use_json) {
#ifdef FEATURE_JSON
if (use_json) {
// JSON-specific code
json_serialize(...);
}
#endif
// Always compiled code
printf("Running\n");
}
Coffee converts feature names to uppercase and adds FEATURE_ prefix:
- Feature
json→-DFEATURE_JSON - Feature
advanced-logging→-DFEATURE_ADVANCED_LOGGING
In Headers
You can provide different APIs based on features:
// mylib.h
#ifdef FEATURE_JSON
void mylib_json_parse(const char *data);
#endif
#ifdef FEATURE_XML
void mylib_xml_parse(const char *data);
#endif
Users of your library can then conditionally call these functions:
#ifdef FEATURE_JSON
mylib_json_parse(data);
#endif
Consuming Libraries with Features
Enabling Features
When you depend on a library that has features, you can choose which to enable via the features field in the dependency’s inline table, or from the command line:
coffee build --features "mylib/json,mylib/logging"
See the Dependency with features enabled example in the Coffee.toml specification.
Disabling Default Features
Use default-features = false in the dependency’s inline table, or pass --no-default-features:
coffee build --no-default-features --features "mylib/xml"
See the Dependency with default features disabled example in the Coffee.toml specification.
Enabling All Features
coffee build --all-features
This enables ALL features from ALL dependencies. Useful for testing.
Feature Resolution Algorithm
When you build with features, Coffee:
- Starts with your explicitly requested features (from
--featuresCLI flag or package dependencies) - Adds default features unless
--no-default-featuresis set - Walks the dependency graph:
- For each dependency, checks if any of its features are required
- If
optional = trueand no features required, skip linking that dependency - If dependency requires specific features (via
dep/featuresyntax), add those to the set
- Unification: If the same crate appears multiple times with different features, union them all
- Generates
-DFEATURE_<NAME>for each feature in the resolved set
Example
Suppose your Coffee.toml declares dependencies a and b, where a defines features x, y (with x as default), and b defines feature z.
You run:
coffee build --features "a/y"
Resolution process:
- Root requests:
a/y - Process dependency
a: features needed:y ahas defaultx, but you explicitly requestedy, so bothxandyare enabled fora- Process dependency
b: no features requested, so uses defaults (or none if no default) - Final feature set:
FEATURE_X,FEATURE_Y(for a’s code), nothing for b
Conditional Compilation Patterns
Feature-Guarded Source Code
You can structure your project:
src/
├── main.c # Always compiled
├── json.c # #ifdef FEATURE_JSON
├── xml.c # #ifdef FEATURE_XML
└── logging.c # #ifdef FEATURE_LOGGING
Each .c file contains feature-gated code. Coffee will compile all .c files regardless, but they can conditionally compile code internally.
Alternatively, you can use build scripts to conditionally include files (future feature).
Feature Detection
In your code, check if a feature is enabled:
#ifdef FEATURE_JSON
#define HAS_JSON 1
#else
#define HAS_JSON 0
#endif
void mylib_init(void) {
#if HAS_JSON
init_json_subsystem();
#endif
}
Common Use Cases
For complete TOML examples of common feature patterns, see the Common Patterns section in the Coffee.toml specification, including:
- Network vs CLI tool (optional curl dependency)
- Database backend selection (postgres/mysql/sqlite)
- Logging levels (multiple feature flags)
Using Logging Levels in Code
#ifdef FEATURE_LOG_TRACE
#define LOG(fmt, ...) printf("[TRACE] " fmt "\n", ##__VA_ARGS__)
#elif defined(FEATURE_LOG_DEBUG)
#define LOG(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__)
#elif defined(FEATURE_LOG_INFO)
#define LOG(fmt, ...) printf("[INFO] " fmt "\n", ##__VA_ARGS__)
#else
#define LOG(fmt, ...) do {} while(0)
#endif
4. Testing Different Configurations
# Build with everything
coffee build --all-features
# Build minimal (only defaults)
coffee build
# Build without logging (smaller binary)
coffee build --no-default-features --features "json"
# Build with specific features
coffee build --features "json,xml,libmysqlclient"
Best Practices
- Use lowercase, hyphenated names:
advanced-logging,postgres-backend - Keep feature names stable: Changing feature names breaks users’ builds
- Document each feature: In README, explain what each feature does
- Avoid feature overlap: If two features enable mostly the same code, merge them
- Test all feature combinations: At least test default + common combos
- Default to minimal: Only enable useful defaults; let users opt-in to more
- Use
optional = truewisely: Mark dependencies optional only if they’re truly feature-specific
Troubleshooting
“Feature ‘x’ not found”
- Check spelling: feature names are case-sensitive
- Ensure the dependency actually defines that feature in its Coffee.toml
- Use
coffee metadatato inspect a package’s available features (once implemented)
“Cannot resolve features for dependency ‘foo’”
- There’s a circular dependency in feature requirements
- Check that you’re not requesting a feature that doesn’t exist
- The dependency might have been installed without its features metadata
My code doesn’t seem to be compiling conditionally
- Ensure
#ifdef FEATURE_NAMEmatches the feature name (uppercase with underscores) - Check that the feature is actually enabled (check build logs for
-DFEATURE_NAME) - Verify the code is in a
.cfile that’s part of the build (all files in src/ are compiled)
Limitations
- Features cannot currently enable/disable specific source files (only code within files)
- No support for target-specific features (e.g.,
cfg(target_os = "linux")) - Build scripts (custom build logic per feature) not yet supported
- Workspace-level feature overrides not yet supported
Future Enhancements
- Build script support: run custom code when features change
dev-dependenciesandbuild-dependencieswith their own features- Target-specific conditional compilation
- Workspace feature overrides at root level
- Feature linting to detect unused features
Related Commands
coffee build- Build with featurescoffee test- Run tests with featurescoffee run- Run binary with featurescoffee metadata- View package featurescoffee generate-lockfile- Generate lockfile with resolved features
Example
See examples/feature-demo/ for a complete working example.
Need help?
Please wait: the project is still pre-alpha!
Cup of Coffee is an open source project inspired by Cargo, Rustup Crates, and Conda-forge.