Skip to content

Add C API for adapters - #679

Draft
timkpaine wants to merge 21 commits into
mainfrom
tkp/capi
Draft

Add C API for adapters#679
timkpaine wants to merge 21 commits into
mainfrom
tkp/capi

Conversation

@timkpaine

@timkpaine timkpaine commented Feb 18, 2026

Copy link
Copy Markdown
Member

Add a C API for ABI stability for external adapters. This should let us (and others) create adapters separate from the csp build process in a stable way.

  • input adapter
  • output adapter
  • managers
  • structs
  • C++ example via C API
  • Rust example via C API
  • Tests
  • Version and version checks

@timkpaine timkpaine added type: feature Issues and PRs related to new features adapter: general Issues and PRs related to input/output adapters in general part: build Issues and PRs related to the build process labels Feb 18, 2026
@timkpaine
timkpaine force-pushed the tkp/capi branch 7 times, most recently from fdc5893 to 39e2929 Compare February 18, 2026 04:16
@timkpaine timkpaine added the tag: needs squash PRs that need to be cleaned/squashed label Feb 18, 2026
@timkpaine
timkpaine force-pushed the tkp/capi branch 2 times, most recently from ede3e85 to c845c31 Compare February 18, 2026 21:58
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Memory safety:
- Guard every push entry point against a type that does not match the
  adapter's declared CspType, which previously reinterpreted the value's
  bytes on the engine thread.
- Validate that a StructField handle belongs to the struct it is applied
  to, accepting overridden inherited fields that share the base layout.
- Reject a null data pointer with a non-zero length rather than
  constructing std::string from it.
- Hold a reference to the PushGroup capsule for as long as the engine
  keeps its raw pointer.
- Contain exceptions at both directions of the boundary: every extern "C"
  entry point, and every invocation of a user callback. Destructors and
  name() swallow, since they cannot throw.

ABI:
- Add abi_version and struct_size to every callback table and adopt them
  size-tolerantly, so a table built against an older header is copied as
  far as it goes and zero-filled beyond.
- Give every public enum explicit values.
- Use CCSP_DATETIME_NONE as the "no more sim data" sentinel; 0 is the
  valid Unix epoch.
- Correct CCspStatusLevel, which was inverted relative to
  csp.adapters.status.Level.
- Build the C API shims into cspimpl so CSP_C_API_EXPORT resolves to
  dllexport on Windows, removing the force_load/whole-archive workaround.

Completeness:
- Implement the 85 declared but undefined functions across CspTime,
  CspString, CspValue and the indexed input accessors, and export them.
- Implement the remaining NOT_IMPLEMENTED stubs, including nested struct
  reads, enum writes, struct and generic value pushes, and the managed
  sim input adapter.
- Reject reads from inputs that have never ticked instead of returning a
  default-constructed value.

Rust example:
- Correct enum discriminants that did not match the C headers, and route
  every FFI return through a checked conversion so an unrecognized value
  cannot become an invalid enum.
- Reject non-positive intervals and wait on a condition variable so stop
  interrupts the worker immediately.
- Cache symbol lookups instead of resolving on every call.

C example:
- Replace the racy stop flag, the unconditional join, the mis-cast
  Windows thread entry, usleep and rand with a portable worker.

Build and CI:
- Make the example builds platform-portable and ship the import library
  Windows consumers link against.
- Add a C API check that every exported declaration resolves, and that
  every public header compiles standalone as C11 and C++20.
- Add gtest coverage for the time, string, value and ABI layers.
- Pin the new actions by SHA and drop the retired duplicate cache.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
ErrorExtern.cpp defines ccsp_set_error but was only listed in the
test_c_api target, so the six other C API translation units in cspimpl
referenced an undefined symbol. ELF and Mach-O permit undefined symbols
in a shared object and resolve them lazily, so the Linux and macOS
wheels linked and only Windows failed, with LNK1120 on _cspimpl.pyd.
The module would have raised ImportError on first use everywhere.

Also define CSPIMPL_EXPORTS for test_c_api. That target compiles the C
API sources itself, so without it CSP_C_API_EXPORT expands to dllimport
for symbols the target defines locally, which MSVC reports as LNK4217
and LNK4286.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
The managed input bridge unpacked four scalars, but only kwargs become
scalars: InputAdapterDef strips push_mode from the tail and AdapterDef
strips the manager from the head, leaving (typ, interval_ms,
push_group). That raised ValueError on macOS in test_cpp_examples.

The managed output bridge had the same miscount, masked rather than
raised: scalars is (prefix,), so `scalars[1] if len(scalars) > 1 else ""`
always took the else branch and the manager's prefix was silently
discarded on every call.

Replace the defensive index lookups with strict unpacking in all four
bridges so a future mismatch fails loudly instead of substituting a
default. Verified the delivered tuples for each of the four adapter
definitions against csp's wiring layer.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
check_c_api.py only searched the repository, so the example CI job
failed with "could not locate the built extension module": that job
installs a csp wheel and builds nothing in-tree.

Search the repository first and fall back to csp.get_lib_path(). Repo
first matters, otherwise an unrelated csp that happens to be installed
is checked instead of the build under test, which reports every symbol
as missing rather than failing outright.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
The example builds searched only for _cspimpl, but CMake applies the
leading underscore to the module and not to its import library, so the
wheel ships csp/lib/_cspimpl.pyd alongside csp/lib/cspimpl.lib. Every
Windows example failed to configure with CSP_LIBRARY-NOTFOUND.

Search both names, preferring the underscored one if a build ever
produces it. Confirmed against the wheel from this branch: cspimpl.lib
is present and exports both the C API symbols and the C++ symbols the
older examples link.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Examples 1 through 3 declare add_library(... SHARED ...) and link only
the csp module, so MSVC failed with LNK1104 on python311.lib: pyconfig.h
emits a pragma naming the import library, but nothing put it on the link
line. Example 4 already builds with Python_add_library, which links
Python::Module for you, which is why it configured.

Development.Module is already requested, so link the target it provides.
On Windows it supplies pythonXY.lib; on Unix it deliberately links no
libpython, leaving those symbols to the host interpreter. Verified on
macOS that all three still build and that none gained a libpython
reference.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
The C++ examples linked only the csp module, which left 20 unresolved
externals on MSVC: csp::Node, csp::Engine, csp::TimeSeriesProvider,
csp::StructMeta and friends are not dllexport'd, so they are absent from
the module's import library. Unix links the module directly and sees
every symbol, which is why this only surfaced on Windows.

Link the static archives the wheel already ships, matching what csp's
own extension modules do in cpp/csp/python/adapters/CMakeLists.txt.
csptypesimpl is included as an import library because
DialectGenericType is declared dllimport and so cannot be satisfied by a
static archive.

Mapped each unresolved symbol group to its providing archive using the
Windows wheel built from this branch, and confirmed all four names
resolve under MSVC library naming. macOS is unaffected: the variable is
empty off Windows and all three examples still build.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
ExampleOutputAdapter.c included unistd.h and wrote through dprintf on a
file descriptor, neither of which MSVC provides, so the Windows build
failed with C1083 on unistd.h.

Write to a FILE * with fprintf instead. That is standard C rather than
POSIX, so it needs no platform branch, and the file descriptor was
incidental to what the example teaches. example_output_adapter_create_fd
becomes example_output_adapter_create_stream; it had no callers outside
this file.

Checked the example's other C sources for the same assumption; the only
other POSIX include is pthread.h, already guarded for _WIN32. Verified
the example builds and its 4 tests pass.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
dumpbin only exists inside a Visual Studio developer environment, which
the CI runner never enters, so the symbol check died with WinError 2
after the example had built and passed its tests.

Parse the PE export name table instead. It needs no toolchain, so the
check keeps working wherever Python runs, and Windows is the platform
this check exists to cover: it is the one that can silently fail to
export a symbol.

Validated against the win_amd64 wheel built from this branch, read on
macOS: 252 exports, of which 219 are ccsp_, matching the 219 declared
symbols exactly. Non-PE input is rejected rather than misparsed.

Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adapter: general Issues and PRs related to input/output adapters in general part: build Issues and PRs related to the build process tag: needs squash PRs that need to be cleaned/squashed type: feature Issues and PRs related to new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant