Project 5: Cross-Platform Shared Library with C API
Build a portable shared library with a stable C API usable from multiple languages.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 2-3 weeks |
| Language | C |
| Prerequisites | C, basic build systems, ABI basics |
| Key Topics | symbol visibility, calling conventions, versioning |
1. Learning Objectives
By completing this project, you will:
- Export a stable C API with correct symbol visibility per platform.
- Build
.so,.dylib, and.dlltargets. - Design an FFI-friendly API with clear ownership rules.
- Version your library without breaking clients.
2. Theoretical Foundation
2.1 Core Concepts
- Symbol visibility:
__attribute__((visibility("default")))vs__declspec(dllexport). - ABI stability: Function signatures and struct layouts must stay compatible.
- FFI design: Avoid complex structs; expose explicit create/free APIs.
2.2 Why This Matters
Most real shared libraries are used across languages. Portability and stable ABI are the difference between a usable library and a support nightmare.
2.3 Historical Context / Background
C APIs remain the universal lingua franca for cross-language interop. Many popular libraries expose a C API for this reason.
2.4 Common Misconceptions
- “C++ APIs are portable”: Name mangling and ABI differences make them fragile.
- “Symbols are exported by default”: Not on Windows or with hidden visibility.
3. Project Specification
3.1 What You Will Build
A small but useful library (e.g., JSON parser or image resize) with a C API and portable build system.
3.2 Functional Requirements
- Provide a C API with explicit create/cleanup functions.
- Export symbols correctly on Linux, macOS, and Windows.
- Build shared libraries on all platforms.
- Demonstrate FFI usage from another language.
3.3 Non-Functional Requirements
- Reliability: Avoid memory leaks and ownership confusion.
- Usability: Simple, documented API.
- Portability: Build scripts work across platforms.
3.4 Example Usage / Output
mylib_parse("{\"hello\":\"world\"}") -> ok
3.5 Real World Outcome
You can load the library from Python or Ruby:
import ctypes
mylib = ctypes.CDLL("./libmylib.so")
res = mylib.parse_json(b'{"hello":"world"}')
4. Solution Architecture
4.1 High-Level Design
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ C API header │────▶│ shared library│────▶│ FFI consumer │
└──────────────┘ └───────────────┘ └──────────────┘
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Public API | Stable interface | Minimal surface area |
| Build system | Multi-platform builds | CMake or Make + scripts |
| FFI example | Validate usability | Python ctypes |
4.3 Data Structures
typedef struct mylib_ctx mylib_ctx_t;
MYLIB_API mylib_ctx_t *mylib_create(void);
MYLIB_API void mylib_destroy(mylib_ctx_t *ctx);
MYLIB_API int mylib_parse(mylib_ctx_t *ctx, const char *json);
4.4 Algorithm Overview
Key Algorithm: API wrapper
- Hide internal structs behind opaque pointers.
- Expose functions for create/use/destroy.
- Keep memory ownership clear.
Complexity Analysis:
- Time: O(N) for input size, depending on library behavior.
- Space: O(N) for parsed structures.
5. Implementation Guide
5.1 Development Environment Setup
cmake --version
gcc --version
5.2 Project Structure
project-root/
├── include/
│ └── mylib.h
├── src/
│ └── mylib.c
├── examples/
│ └── python_ctypes.py
└── CMakeLists.txt
5.3 The Core Question You’re Answering
“How do I design a shared library that works across platforms and languages?”
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Symbol export macros
MYLIB_APIper platform
- Calling conventions
- cdecl vs stdcall (Windows)
- Ownership rules
- Who allocates and frees memory
5.5 Questions to Guide Your Design
- Which functions should be in the public API?
- How will you keep ABI stable across versions?
- How will you test FFI compatibility?
5.6 Thinking Exercise
If you add a new field to a public struct, how might that break existing consumers?
5.7 The Interview Questions They’ll Ask
- Why is a C API more portable than C++?
- How do you export symbols on Windows?
- What is an opaque pointer and why use it?
5.8 Hints in Layers
Hint 1: Export macro
- Define
MYLIB_APIin the header.
Hint 2: Opaque types
- Forward declare structs in the public header.
Hint 3: Versioning
- Embed
mylib_version()and soname rules.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| C API design | “C Interfaces and Implementations” | API design chapters |
| ABI basics | “Linkers and Loaders” | ABI sections |
| FFI patterns | “Programming Rust” | FFI chapter |
5.10 Implementation Phases
Phase 1: Foundation (4-5 days)
Goals:
- Build the library on one platform.
Tasks:
- Create
mylib.handmylib.c. - Export symbols with correct visibility.
Checkpoint: Library builds and tests locally.
Phase 2: Core Functionality (5-7 days)
Goals:
- Make it cross-platform.
Tasks:
- Add Windows and macOS build targets.
- Validate symbol export.
Checkpoint: Builds produce .so, .dylib, .dll.
Phase 3: Polish & Edge Cases (3-5 days)
Goals:
- Validate FFI usage and versioning.
Tasks:
- Add Python ctypes example.
- Add versioning strategy.
Checkpoint: Python uses the library successfully.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Public types | structs vs opaque | opaque | ABI stability |
| Build system | CMake vs custom | CMake | Cross-platform support |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Build | Cross-platform output | .so/.dylib/.dll |
| API | Functional correctness | parse returns ok |
| FFI | Validate bindings | Python ctypes call |
6.2 Critical Test Cases
- All symbols are exported correctly.
- FFI call returns expected result.
- No memory leaks in repeated calls.
6.3 Test Data
{"hello":"world"}
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Hidden symbols | FFI load fails | Fix export macro |
| ABI breaks | Crash in clients | Use opaque types |
| Ownership confusion | Leaks or double-free | Document ownership rules |
7.2 Debugging Strategies
- Use
nmordumpbinto inspect exported symbols. - Add versioned API tests.
7.3 Performance Traps
Excessive copying across the API boundary can slow performance.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a second API function.
8.2 Intermediate Extensions
- Add a stable error reporting API.
8.3 Advanced Extensions
- Bindings for two languages (Python + Rust).
9. Real-World Connections
9.1 Industry Applications
- Libraries: Many core libraries expose C APIs for compatibility.
- SDKs: Provide stable ABI for third-party users.
9.2 Related Open Source Projects
- zlib: Simple C API used everywhere.
- libpng: Classic portable shared library.
9.3 Interview Relevance
- Demonstrates knowledge of ABI stability and portability.
10. Resources
10.1 Essential Reading
- C Interfaces and Implementations - API design.
- Drepper - Symbol visibility guidance.
10.2 Video Resources
- Search: “C API design shared library”.
10.3 Tools & Documentation
- CMake: cross-platform build docs.
- ctypes: Python FFI.
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain ABI vs API.
- I can export symbols on Windows and Linux.
11.2 Implementation
- Library builds on multiple platforms.
- FFI call works.
11.3 Growth
- I can design a stable C API for a larger project.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Build the library and call it from a C test.
Full Completion:
- Use it from a foreign language via FFI.
Excellence (Going Above & Beyond):
- Multi-language bindings and versioned releases.
This guide was generated from SHARED_LIBRARIES_LEARNING_PROJECTS.md. For the complete learning path, see the parent directory README.