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:

  1. Export a stable C API with correct symbol visibility per platform.
  2. Build .so, .dylib, and .dll targets.
  3. Design an FFI-friendly API with clear ownership rules.
  4. 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

  1. Provide a C API with explicit create/cleanup functions.
  2. Export symbols correctly on Linux, macOS, and Windows.
  3. Build shared libraries on all platforms.
  4. 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

  1. Hide internal structs behind opaque pointers.
  2. Expose functions for create/use/destroy.
  3. 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:

  1. Symbol export macros
    • MYLIB_API per platform
  2. Calling conventions
    • cdecl vs stdcall (Windows)
  3. Ownership rules
    • Who allocates and frees memory

5.5 Questions to Guide Your Design

  1. Which functions should be in the public API?
  2. How will you keep ABI stable across versions?
  3. 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

  1. Why is a C API more portable than C++?
  2. How do you export symbols on Windows?
  3. What is an opaque pointer and why use it?

5.8 Hints in Layers

Hint 1: Export macro

  • Define MYLIB_API in 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:

  1. Create mylib.h and mylib.c.
  2. Export symbols with correct visibility.

Checkpoint: Library builds and tests locally.

Phase 2: Core Functionality (5-7 days)

Goals:

  • Make it cross-platform.

Tasks:

  1. Add Windows and macOS build targets.
  2. Validate symbol export.

Checkpoint: Builds produce .so, .dylib, .dll.

Phase 3: Polish & Edge Cases (3-5 days)

Goals:

  • Validate FFI usage and versioning.

Tasks:

  1. Add Python ctypes example.
  2. 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

  1. All symbols are exported correctly.
  2. FFI call returns expected result.
  3. 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 nm or dumpbin to 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.
  • 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.

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.