Adding Runtime Features

Add the smallest correct compatibility slice while preserving native ownership, graph visibility, platform honesty, and reproducible evidence.

Contributor workflowCMake ownershipTests + boundaries

Start with evidence, not a status label

A new API is complete only when its behavior, physical ownership, dependency visibility, tests, platform boundary, and documentation agree. Do not start from an old Todo/Stub/Partial badge or assume that a matching header comment is authoritative. Read the implementation around the proposed owner, its current CMake registration, its tests, and the upstream .NET contract when compatibility is intended.

Write a short contract note before coding: supported overloads, parameter validation and precedence, return behavior, exceptions, ownership, mutation, concurrency, platform behavior, and deliberate exclusions. This makes review possible without pretending the entire upstream type will arrive in one patch.

1. Identify the .NET contract and the compatibility slice

Use current official .NET reference/source material as conceptual evidence, then decide which observable slice belongs in Sharp Runtime. Matching .NET can include argument validation order, special numeric values, collection versioning, cancellation transitions, or error translation—not only the happy-path result.

QuestionRecord
What exact API is requested?Names, templates, overloads, default values, and property naming
What behavior matters?Normal, boundary, invalid, empty, disposed, concurrent, and platform cases
What cannot be reproduced?CLR metadata, GC lifetime, managed encoding units, unavailable OS service
What is the native representation?Value, unique/shared owner, borrow, span, native handle, callback
How will callers verify it?Public tests, examples, and a precise deviation/limitation note

2. Select the owning physical component

Search the generated API inventory for neighboring types and inspect cmake/SharpRuntimeModules.cmake. One component must own every public header and production source. Do not place an API into a broad compatibility target; Core, Collections, Xml.XPath, and All do not own source.

Use the narrowest coherent owner. A JSON/HTTP extension belongs in Net.Http.Json, not Net.Http merely because it sends a request. A checksum belongs in IO.Hashing, not Security.Cryptography. A public dependency is justified by the public header surface, not by convenience.

3. Inspect dependency direction before including

Draw the required edges before implementation:

  • PUBLIC_DEPENDENCIES when the public include/API exposes that component.
  • PRIVATE_DEPENDENCIES when only a compiled implementation uses it.
  • TEST_DEPENDENCIES when only tests need a helper or cross-component oracle.

Header-only INTERFACE components cannot hide a private dependency. If a new include creates a cycle, do not bypass the validator: move a small contract downward, separate an adapter upward, or redesign the surface.

4. Add header and implementation in the owner

Public declarations go beneath modules/<owner>/include in the namespace-shaped path. Compiled definitions go beneath src. Tests go beneath the same module’s tests. Include the MIT SPDX header and follow neighboring header guards, namespaces, names, and documentation style.

// modules/io-hashing/include/System/IO/Hashing/ExampleHash.hpp
#pragma once

#include <span>
#include <cstdint>

namespace System::IO::Hashing
{
    class ExampleHash final
    {
    public:
        [[nodiscard]] static std::uint32_t Hash(
            std::span<const std::uint8_t> bytes) noexcept;
    };
}

The sample illustrates placement, not a proposal to duplicate an existing algorithm. Prefer current project types where compatibility requires them, but do not introduce a pointer-without-length boundary when a span can express capacity.

5. Preserve naming without hiding semantics

Public names generally follow the familiar .NET type/member casing and the project’s getNameProperty/setNameProperty convention. Use DDATA/IDATA only for simple member-backed properties; an explicit accessor is clearer when validation, allocation, I/O, blocking, or ownership transfer occurs.

Interfaces need virtual destructors. Overrides use override. Mark values [[nodiscard]] where ignoring the result is likely a defect. Use noexcept only when the complete implementation—including allocation, callbacks, and translation—can honor it.

6. Define native ownership and lifetime

Document every pointer, reference, span, callback, task, and handle. State who owns it, how long a borrow lasts, whether an operation stores it, and which mutation invalidates it. Prefer values and RAII, then unique_ptr, then shared_ptr only for real shared lifetime.

Pay special attention to asynchronous and callback APIs:

  • Never capture raw this into work that can outlive the object without a teardown proof.
  • Do not retain a caller buffer unless the API owns/copies it or retains an explicit owner.
  • Prevent a state object from strongly owning a callback that strongly owns the same state.
  • Define cancellation and join order, including calls made from the worker/callback thread itself.
  • Decide how user-handler exceptions propagate or are reported.

7. Implement validation and exception translation

Validate public arguments before mutation or native side effects, and test precedence when more than one input is invalid. Avoid signed-overflow guards such as start + length > size; validate start, then compare length with size - start. Raw pointers cannot prove capacity, so prefer sized types or document the residual responsibility.

Translate native failures at the component boundary into the established Sharp Runtime exception type. Preserve a causal exception or native error where the hierarchy supports it. Unsupported platforms should throw PlatformNotSupportedException or be intentionally excluded; do not silently succeed.

8. Register CMake ownership

Existing module registration discovers src/*.cpp and tests/*.cpp recursively. A typical compiled module registration is concise:

sharp_runtime_register_module(
    NAME IO.Hashing
    TARGET sharp_runtime_io_hashing
    TYPE STATIC
    PUBLIC_DEPENDENCIES Core.Base IO
)

If adding an entirely new physical module, add its directory once to sharp_runtime_module_directories, create its module CMakeLists.txt, and choose STATIC or INTERFACE honestly. CMake rejects missing sources for STATIC, sources/private edges for INTERFACE, duplicate names, invalid include owners, and mixed legacy/new dependency syntax.

9. Add behavior tests in the owning module

Tests are registered into the owning component executable. Cover more than examples:

Test familyExamples
Normal behaviorRepresentative inputs, results, state transitions
BoundaryEmpty, one element, limits, exact end range
InvalidNegative, null/empty callable, malformed encoding, invalid state
PrecedenceTwo invalid conditions; prove which exception wins and no mutation occurred
LifetimeOwner destruction, leave-open, disposal, retained views/callbacks
NumericOverflow, NaN, infinity, signed zero, narrowing
ConcurrencyReentrancy, racing configuration, cancellation, join, repeat under TSan
PlatformSupported branch and explicit unsupported result

Do not count TEST macros as discovered cases. Typed and parameterized suites expand at GoogleTest discovery time. Build and run the actual executable.

export SHARP_RUNTIME_BUILD_JOBS=1
cmake -S . -B build-feature \
  -DSHARP_RUNTIME_COMPONENTS=IO.Hashing \
  -DSHARP_RUNTIME_BUILD_TESTS=ON \
  -DCMAKE_BUILD_TYPE=Debug
cmake --build build-feature \
  --target SharpRuntimeTests_IO_Hashing --parallel 1
./build-feature/SharpRuntimeTests_IO_Hashing

10. Add positive and negative consumer fixtures when relevant

A unit test that compiles inside the repository sees test dependencies and repository paths. A consumer fixture proves the public target in isolation. Add a positive fixture when a new header/target combination must compile and link. Add a negative fixture when a template constraint, forbidden include, private dependency, or invalid construction must fail.

Fixtures live in test/consumer. The shared consumer CMake project enables only one requested component and collects only consumer-visible usage requirements in compile-only mode. Negative checks require the expected failure category; an unrelated compiler error does not count as success.

export SHARP_RUNTIME_BUILD_JOBS=1
scripts/check_selective_components.sh \
  IO.Hashing io_hashing.cpp
python3 scripts/check_negative_consumer_fixtures.py --jobs 1

11. Validate module boundaries

python3 scripts/validate_module_boundaries.py
python3 test/validate_module_boundaries_test.py
python3 scripts/generate_component_catalog.py --check

The validator checks unique ownership, source registration, include and link edges, visibility, cycles, and related invariants. Fix the architecture when it fails. Do not add a global include directory or an allow-list exception merely to preserve a misplaced header.

12. Handle platform behavior explicitly

For an OS-backed API, record separate evidence for source implementation, configure, build, runtime, and tests. A preprocessor branch is source support only. Add a supported implementation or a specific unsupported result. Keep Windows/POSIX error translation and resource cleanup symmetrical where possible.

When behavior differs legitimately—path case, terminal facilities, socket error, time-zone data—document the difference and test per platform. Do not label macOS “same as Linux” without an actual macOS path and result.

13. Document deviations and limitations

Update the closest conceptual or class page with units, ownership, comparison policy, encoding, thread safety, and platform scope. Add a limitation only when it is material to users. Remove a limitation when the pinned implementation and tests fix it. Do not publish internal ticket prose as a public limitation without translating it into observable behavior.

Avoid unsupported maturity scores. Prefer statements such as “Linux/inotify-only; recursive watching is not implemented” or “the dedicated module has no test executable at this pin.” These are evidence a reader can act on.

14. Regenerate catalogues and API discovery

python3 scripts/generate_component_catalog.py

# In the website checkout, using the explicit pinned runtime checkout:
python3 tools/sync_components.py --runtime /path/to/pinned/runtime
python3 tools/sync_api_inventory.py --runtime /path/to/pinned/runtime

The component catalogue records owners and dependency edges. The API inventory walks public include trees under those owners. Both embed the exact SHA. Never regenerate them from an arbitrary active branch.

15. Run the local gate with an explicit job budget

export SHARP_RUNTIME_BUILD_JOBS=1
scripts/local_ci_check.sh build-local

The local gate validates the graph, validator tests, catalogue, version seam, negative consumers, configure, a warning-clean build, and component tests. Its shared job policy has a hard ceiling; use a conservative explicit budget on a shared or memory-limited host. A failure must remain visible—do not convert an environment-sensitive network result into a green claim.

16. Update documentation and website facts

If the change adds a public header, component, edge, test executable, discovered tests, platform result, or measured gate, update the canonical generated data and source audit. Add or compile examples for a user-facing workflow. Regenerate search, sitemap, metadata, and the static pages. Run the website drift verifier against the same explicit SHA.

Adding a new component

  1. Create modules/<directory>/include, optional src, tests, and a module CMakeLists.txt.
  2. Register its NAME, target, kind, and exact public/private/test edges.
  3. Add the directory to sharp_runtime_module_directories.
  4. Add positive isolated-consumer coverage and absence assertions for unrelated targets.
  5. Add negative fixtures for private headers or generic constraints where relevant.
  6. Regenerate the component catalogue and run boundary validation.
  7. Add the component purpose/platform guidance and regenerate the API inventory.

Review checklist

  • The public slice and intentional reductions are written down.
  • Exactly one physical component owns every new header/source.
  • Public/private/test dependencies reflect visibility, not convenience.
  • Ownership, borrowing, async capture, disposal, and thread safety are documented.
  • Argument validation cannot overflow and happens before mutation.
  • Normal, invalid, boundary, lifetime, platform, and concurrency behavior is tested as applicable.
  • Consumer fixtures prove the public boundary and reject forbidden surfaces.
  • Component catalogue, API inventory, limitations, tutorials, and source audit are current.
  • The local gate ran with a safe job budget and its exact outcome is recorded.