Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ venv/
*.vscode_debug_path/
lib/*

# Headers staged by sdk/tools/sync_headers.py (duplicates of src/)
sdk/src/bsk_sdk/include/Basilisk/architecture/_GeneralModuleFiles/
sdk/src/bsk_sdk/include/Basilisk/architecture/messaging/
sdk/src/bsk_sdk/include/Basilisk/architecture/utilities/

# Python packaging
*.egg-info
build/
Expand Down
1 change: 1 addition & 0 deletions docs/source/Support/Developer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ The following support files help with writing Basilisk modules.
Developer/createHtmlDocumentation
Developer/bskModuleCheckoutList
Developer/UnderstandingBasilisk
Developer/bskSdkV1
Developer/migratingBskModuleToBsk2
Developer/MigratingToPython3
162 changes: 162 additions & 0 deletions docs/source/Support/Developer/bskSdkV1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
Basilisk SDK Version 1
======================

.. contents:: Outline
:local:

Purpose
-------

The Basilisk SDK (``bsk-sdk``) defines the public surface that external plugin
authors can rely on when integrating new simulation capabilities with the
core runtime. Version 1 focuses on establishing a stable contract for Python
and C++ plugin authors and capturing the minimal tooling that ships inside the
Basilisk source tree.

Scope and Deliverables
----------------------

Version 1 guarantees the following artifacts:

- ``bsk_core.plugins``: the runtime registry responsible for discovering
entry-point advertised plugins and exposing them under ``Basilisk.modules``.
- ``bsk-sdk``: a small Python package that publishes the SDK headers, declares a
dependency on the ``pybind11`` headers required by the helper macros, and
provides :func:`bsk_sdk.include_dir` / :func:`bsk_sdk.include_dirs` helpers for
build scripts.
- A companion ``sync_headers.py`` utility (``sdk/tools``) keeps the vendored
Basilisk ``architecture`` headers in sync with the main source tree.
- ``sdk/include/bsk/plugin_sdk.hpp``: a single header that wraps the pybind11
boilerplate required for C++ factories and enforces the default constructible
+ ``Reset``/``UpdateState`` interface contract. The same header is shipped by
:mod:`bsk-sdk`.
- A consolidated ``plugins`` example package containing both Python and C++
implementations that demonstrate the expected packaging and registration
patterns.

Any other files in the repository are explicitly *not* part of the SDK
agreement for this release.

Plugin Registry API
-------------------

The ``bsk_core.plugins.PluginRegistry`` class is the primary integration
point for third-party plugins. The registry is responsible for staging plugin
definitions until the runtime exports them under ``Basilisk.modules``.

The public methods guaranteed in v1 are:

.. code-block:: python

class PluginRegistry:
def register_python_module(self, name: str, cls: type[sysModel.SysModel]) -> None: ...
def register_factory(self, name: str, factory: Any) -> None: ...

``register_python_module`` accepts any subclass of
``Basilisk.architecture.sysModel.SysModel`` and exposes it as a class on
``Basilisk.modules`` using the provided name. ``register_factory`` stores an
opaque object under the supplied name. Factories are expected to be callables
returning Basilisk-compatible module instances, but v1 defers any runtime shape
validation to keep the surface area small.

Plugins must advertise a ``register(registry)`` callable through the
``basilisk.plugins`` entry-point group. During startup Basilisk resolves the
entry-point, imports the containing module, and invokes the callable with the
shared registry instance.

Python Plugin Pattern
---------------------

Pure-Python plugins should follow the pattern demonstrated in
``plugins/src/python/Basilisk/ExternalModules/customPythonModule.py``:

.. code-block:: python

from Basilisk.architecture import sysModel

class ExamplePluginModule(sysModel.SysModel):
def Reset(self, current_sim_nanos):
...

def UpdateState(self, current_sim_nanos, call_time):
...

def register(registry):
registry.register_python_module("ExamplePluginModule", ExamplePluginModule)

The distribution's ``pyproject.toml`` must expose the ``register`` function via

.. code-block:: toml

[project.entry-points."basilisk.plugins"]
example = "bsk_example_plugin.simple:register"

At runtime users import the module from ``Basilisk.modules``:

.. code-block:: python

from Basilisk import modules

plugin_cls = modules.ExamplePluginModule
instance = plugin_cls()
instance.Reset(0)
instance.UpdateState(0, 0)

C++ Plugin Pattern
------------------

Native extensions should include ``sdk/include/bsk/plugin_sdk.hpp`` to inherit
the pybind11 binding helpers. When building outside the Basilisk source tree
the :mod:`bsk-sdk` package exposes the headers via
``import bsk_sdk; bsk_sdk.include_dir()`` (or ``include_dirs()`` to also capture
the ``Basilisk`` subdirectory and ``pybind11`` include path). Version 1
guarantees the availability of:

- ``bsk::plugin::register_basic_plugin``
- ``BSK_PLUGIN_PYBIND_MODULE``

The ``BSK_PLUGIN_PYBIND_MODULE`` macro defines both the pybind11 module and the
``create_factory`` callable consumed by the Basilisk runtime. The expected class
contract mirrors the Python case: default constructible with ``Reset`` and
``UpdateState`` methods.

.. code-block:: cpp

#include <bsk/plugin_sdk.hpp>

class ExampleCppModule {
public:
void Reset(double current_sim_nanos);
void UpdateState(double current_sim_nanos, double call_time);
};

BSK_PLUGIN_PYBIND_MODULE(_example_cpp, ExampleCppModule, "ExampleCppModule");

The companion Python package should lazily import the extension, extract the
factory, and register it:

.. code-block:: python

from importlib import import_module

def register(registry):
ext = import_module("bsk_example_plugin_cpp._example_cpp")
factory = ext.create_factory()
registry.register_factory("ExampleCppFactory", factory)

Limitations and Future Work
---------------------------

Version 1 intentionally leaves several items out of scope so they can be
designed with real-world feedback:

- The SDK header is distributed from the Basilisk source tree and is not
published as a standalone artifact.
- Factories registered via ``register_factory`` are treated as opaque callables;
Basilisk does not verify their type or interface beyond name collisions.
- The helper header requires C++17 and a compatible pybind11 toolchain.
- Plugin lifecycle hooks beyond ``Reset``/``UpdateState`` will be designed as
future Basilisk modules adopt richer interfaces.

Feedback on these gaps is welcome and will inform the roadmap for subsequent
SDK revisions.
65 changes: 65 additions & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
cmake_minimum_required(VERSION 3.18)
project(bsk_external LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)

execute_process(
COMMAND "${Python3_EXECUTABLE}" -c "import bsk_sdk; print('\\n'.join(bsk_sdk.include_dirs()), end='')"
OUTPUT_VARIABLE BSK_SDK_INCLUDE_OUTPUT
RESULT_VARIABLE BSK_SDK_RESULT
)
if(NOT BSK_SDK_RESULT EQUAL 0)
message(FATAL_ERROR "Failed to locate bsk-sdk include directories. Is the package installed?")
endif()
string(REPLACE "\r" "" BSK_SDK_INCLUDE_OUTPUT "${BSK_SDK_INCLUDE_OUTPUT}")
string(STRIP "${BSK_SDK_INCLUDE_OUTPUT}" BSK_SDK_INCLUDE_OUTPUT)
string(REPLACE "\n" ";" BSK_SDK_INCLUDE_DIRS "${BSK_SDK_INCLUDE_OUTPUT}")
list(GET BSK_SDK_INCLUDE_DIRS 1 BSK_SDK_BASILISK_INCLUDE)

Python3_add_library(_custom_cpp MODULE ExternalModules/CustomCppModule/custom_cpp_module.cpp WITH_SOABI)
target_compile_features(_custom_cpp PRIVATE cxx_std_17)
target_include_directories(
_custom_cpp
PRIVATE
${BSK_SDK_INCLUDE_DIRS}
)
target_link_libraries(_custom_cpp PRIVATE Python3::Module)

set(BSK_ARCHITECTURE_SOURCES
"${BSK_SDK_BASILISK_INCLUDE}/architecture/_GeneralModuleFiles/sys_model.cpp"
"${BSK_SDK_BASILISK_INCLUDE}/architecture/utilities/bskLogging.cpp"
"${BSK_SDK_BASILISK_INCLUDE}/architecture/utilities/moduleIdGenerator/moduleIdGenerator.cpp"
)

target_sources(_custom_cpp PRIVATE ${BSK_ARCHITECTURE_SOURCES})

install(
TARGETS _custom_cpp
DESTINATION Basilisk/ExternalModules
COMPONENT extensions
)

install(
DIRECTORY src/python/
DESTINATION .
COMPONENT python
)

install(
DIRECTORY msgPayloadDefC msgPayloadDefCpp
DESTINATION Basilisk
COMPONENT python
)

if(APPLE)
# 1) Don't hide symbols for this target (avoid losing PyInit export)
set_target_properties(_custom_cpp PROPERTIES
C_VISIBILITY_PRESET default
CXX_VISIBILITY_PRESET default
VISIBILITY_INLINES_HIDDEN OFF
)

endif()
116 changes: 116 additions & 0 deletions plugins/ExternalModules/CustomCppModule/custom_cpp_module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <vector>

#include <Basilisk/architecture/_GeneralModuleFiles/sys_model.h>
#include <Basilisk/architecture/messaging/messaging.h>

#include <bsk/plugin_sdk.hpp>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

namespace {

struct CustomPluginMsgPayload
{
std::array<double, 3> dataVector{};
};

class CustomCppModule : public SysModel
{
public:
CustomCppModule()
: output_writer_(dataOutMsg.addAuthor())
, input_reader_(input_channel_.addSubscriber())
, input_writer_(input_channel_.addAuthor())
{
this->ModelTag = "CustomCppModule";
}

void Reset(uint64_t /*current_sim_nanos*/) override
{
reset_called_ = true;
update_called_ = false;
steps_ = 0;
last_input_ = {};
last_output_ = {};
last_update_nanos_ = 0;
}

void UpdateState(uint64_t current_sim_nanos) override
{
update_called_ = true;
++steps_;

if (input_reader_.isLinked() && input_reader_.isWritten()) {
last_input_ = input_reader_();
}

last_output_ = last_input_;
last_output_.dataVector[0] += static_cast<double>(steps_);
last_output_.dataVector[2] = static_cast<double>(current_sim_nanos) * 1e-9;

output_writer_(&last_output_, this->moduleID, current_sim_nanos);
last_update_nanos_ = current_sim_nanos;
}

void set_input_payload(CustomPluginMsgPayload payload)
{
// WriteFunctor wants a non-const pointer; payload is local, so OK.
input_writer_(&payload, this->moduleID, last_update_nanos_);
}

CustomPluginMsgPayload last_input() const { return last_input_; }
CustomPluginMsgPayload last_output() const { return last_output_; }
uint64_t last_update_nanos() const { return last_update_nanos_; }
bool reset_called() const { return reset_called_; }
bool update_called() const { return update_called_; }

private:
Message<CustomPluginMsgPayload> dataOutMsg;
Message<CustomPluginMsgPayload> input_channel_;

WriteFunctor<CustomPluginMsgPayload> output_writer_;
ReadFunctor<CustomPluginMsgPayload> input_reader_;
WriteFunctor<CustomPluginMsgPayload> input_writer_;

CustomPluginMsgPayload last_input_{};
CustomPluginMsgPayload last_output_{};

uint64_t last_update_nanos_ = 0;
bool reset_called_ = false;
bool update_called_ = false;
int steps_ = 0;
};

} // namespace

PYBIND11_MODULE(_custom_cpp, m)
{
namespace py = pybind11;

py::class_<CustomPluginMsgPayload>(m, "CustomPluginMsgPayload")
.def(py::init<>())
.def(py::init([](const std::vector<double>& values) {
CustomPluginMsgPayload payload;
for (std::size_t i = 0; i < std::min(values.size(), payload.dataVector.size()); ++i) {
payload.dataVector[i] = values[i];
}
return payload;
}))
.def_readwrite("dataVector", &CustomPluginMsgPayload::dataVector);

py::class_<CustomCppModule>(m, "CustomCppModule")
.def(py::init<>())
.def("Reset", &CustomCppModule::Reset, py::arg("current_sim_nanos"))
.def("UpdateState", &CustomCppModule::UpdateState, py::arg("current_sim_nanos"))
.def("set_input_payload", &CustomCppModule::set_input_payload, py::arg("payload"))
.def_property_readonly("last_input", &CustomCppModule::last_input)
.def_property_readonly("last_output", &CustomCppModule::last_output)
.def_property_readonly("last_update_nanos", &CustomCppModule::last_update_nanos)
.def_property_readonly("reset_called", &CustomCppModule::reset_called)
.def_property_readonly("update_called", &CustomCppModule::update_called);

m.def("create_factory", []() { return bsk::plugin::make_factory<CustomCppModule>(); });
}
Loading
Loading