Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40c9103
Build system for python-wrapper
Mar 14, 2025
7302be2
Added wrapper dependencies to Dockerfiles
Mar 14, 2025
3703c20
Python-Wrapper bindings
Mar 14, 2025
76ec913
Added Tests for the Python-Wrapper
Mar 14, 2025
24abdb7
node.cpp Bugfix
Mar 14, 2025
640e815
CMake Python-Wrapper test integration, build fix
Mar 24, 2025
862d498
Applied changes as per feedback.
Mar 24, 2025
c48be8f
Merge branch 'master' into python-wrapper
al3xa23 Apr 7, 2025
73ce98f
Fixed typo
Apr 8, 2025
168df21
Explicitly disallowed copy and assignment
Apr 8, 2025
e710de5
CMakeLists.txt fix
Apr 8, 2025
b1e9a2f
Removed unnecessary dockerfile dependencies
May 14, 2025
365f88b
Renamed wrapper -> binding, LLVM format
May 14, 2025
6806d24
Changes for pipeline test integration
May 14, 2025
c5026a2
Merge branch 'master' into python-wrapper
al3xa23 May 15, 2025
9216366
Binding test formatted with black
May 15, 2025
2d7628f
Format Python binding test with black-jupyter
May 22, 2025
07cffec
Align python-devel package for consistency
Jun 12, 2025
d9d12de
Moved clients/python-binding/ to python/binding/
Jul 17, 2025
a146ecb
python-binding unit test split, socket fixes
Jul 17, 2025
22714fe
unit tests: add test and enable test discovery
Sep 8, 2025
052f419
integration tests: added and enable test discovery
Sep 8, 2025
be2b770
python binding: add wrapper and binding tweaks
Sep 9, 2025
5b887a4
binding tests: add binding wrapper tests
Sep 9, 2025
279eb80
binding tests: CI fix and reformat
Sep 9, 2025
66e1f14
binding wrapper: CI/build fix, add binding stubs
Sep 10, 2025
f73fc67
binding stubs: add missing SPDX header
Sep 12, 2025
124a1f5
python binding: install and fix CI
Sep 12, 2025
f023f4d
Merge branch 'master' into python-wrapper
Sep 12, 2025
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
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ test:unit:
script:
- cmake -S . -B build ${CMAKE_OPTS}
- cmake --build build ${CMAKE_BUILD_OPTS} --target run-unit-tests run-unit-tests-common
- cmake --build build ${CMAKE_BUILD_OPTS} --target run-python-wrapper-tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- cmake --build build ${CMAKE_BUILD_OPTS} --target run-unit-tests run-unit-tests-common
- cmake --build build ${CMAKE_BUILD_OPTS} --target run-python-wrapper-tests
- cmake --build build ${CMAKE_BUILD_OPTS} --target run-unit-tests run-unit-tests-common run-python-wrapper-tests

needs:
- job: "build:source: [fedora]"
artifacts: true
Expand Down
1 change: 1 addition & 0 deletions clients/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
# SPDX-License-Identifier: Apache-2.0

add_subdirectory(opal-rt/rtlab-asyncip)
add_subdirectory(python-wrapper)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually such language support interfaces are called "bindings". Hence also the name "pybind".

I propose to rename this to "python-binding". Its not really wrapping anything in a sense that it builds on top or extends VILLASnode's functionality.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in: 365f88b 6806d24

add_subdirectory(shmem)
39 changes: 39 additions & 0 deletions clients/python-wrapper/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Python-wrapper CMakeLists.
#
# Author: Kevin Vu te Laar <[email protected]>
# SPDX-FileCopyrightText: 2014-2025 Institute for Automation of Complex Power Systems, RWTH Aachen University
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.15...3.29)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this to the top-level CMakeLists.txt? Or is there a reasoning for having this also here?

Building this as a dedicated project wont work due to the missing villas library target which you use below..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in: 6806d24

project(villas-python-wrapper LANGUAGES CXX)

set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG)

if(pybind11_FOUND)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)

execute_process(
COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('stdlib') + '/lib-dynload')"
OUTPUT_VARIABLE PYTHON_LIB_DYNLOAD_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)

message(STATUS "Found Python version: ${Python_VERSION}")
message(STATUS "Python major version: ${Python_VERSION_MAJOR}")
message(STATUS "Python minor version: ${Python_VERSION_MINOR}")
message(STATUS "Python .so install directory: ${PYTHON_LIB_DYNLOAD_DIR}")

pybind11_add_module(python-wrapper villas-python-wrapper.cpp)
set_target_properties(python-wrapper PROPERTIES OUTPUT_NAME villas_node)
target_link_libraries(python-wrapper PUBLIC villas)

install(
TARGETS python-wrapper
COMPONENT lib
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${PYTHON_LIB_DYNLOAD_DIR}
)
else()
message(STATUS "pybind11 not found. Skipping Python wrapper build.")
endif()
246 changes: 246 additions & 0 deletions clients/python-wrapper/villas-python-wrapper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/* Python-wrapper.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* Python-wrapper.
/* Python bindings.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in: 365f88b

*
* Author: Kevin Vu te Laar <[email protected]>
* SPDX-FileCopyrightText: 2014-2025 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/

#include <jansson.h>
#include <pybind11/pybind11.h>
#include <uuid/uuid.h>
#include <villas/node.hpp>
#include <villas/sample.hpp>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate headers in the following three sections:

  • System headers
  • Third-party library headers
  • VILLASnode headers
Suggested change
#include <jansson.h>
#include <pybind11/pybind11.h>
#include <uuid/uuid.h>
#include <villas/node.hpp>
#include <villas/sample.hpp>
#include <jansson.h>
#include <pybind11/pybind11.h>
#include <uuid/uuid.h>
#include <villas/node.hpp>
#include <villas/sample.hpp>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in: 365f88b


extern "C" {
#include <villas/node.h>
}

namespace py = pybind11;

class Array {
public:
Array(unsigned int len) {
smps = new vsample *[len]();
this->len = len;
}
Array(const Array&) = delete;
Array& operator= (const Array&) = delete;

~Array() {
for(unsigned int i = 0; i < len; ++i) {
sample_decref(smps[i]);
smps[i] = nullptr;
}
delete[] smps;
}

vsample * &operator[](unsigned int idx) {
return smps[idx];
}

vsample * &operator[](unsigned int idx) const {
return smps[idx];
}

vsample ** get_smps() {
return smps;
}

unsigned int size() const {
return len;
}

private:
vsample **smps;
unsigned int len;
};

/* pybind11 can not deal with (void **) as function input parameters,
* therefore we have to cast a simple (void *) pointer to the corresponding type
*
* wrapper bindings, sorted alphabetically
* @param villas_node Name of the module to be bound
* @param m Access variable for modifying the module code
*/
PYBIND11_MODULE(villas_node, m) {
m.def("memory_init", &memory_init);

m.def("node_check", [](void *n) -> int {
return node_check((vnode *)n);
});

m.def("node_destroy", [](void *n) -> int {
return node_destroy((vnode *)n);
});

m.def("node_details", [](void *n) -> const char * {
return node_details((vnode *)n);
},
py::return_value_policy::copy);

m.def("node_input_signals_max_cnt", [](void *n) -> unsigned {
return node_input_signals_max_cnt((vnode *)n);
});

m.def("node_is_enabled", [](void *n) -> bool {
return node_is_enabled((const vnode*)n);
});

m.def("node_is_valid_name", [](const char *name) -> bool {
return node_is_valid_name(name);
});

m.def("node_name", [](void *n) -> const char * {
return node_name((vnode *)n);
},
py::return_value_policy::copy);

m.def("node_name_full", [](void *n) -> const char * {
return node_name_full((vnode *)n);
},
py::return_value_policy::copy);

m.def("node_name_short", [](void *n) -> const char * {
return node_name_short((vnode *)n);
},
py::return_value_policy::copy);

m.def("node_netem_fds", [](void *n, int fds[]) -> int {
return node_netem_fds((vnode *)n, fds);
});

m.def("node_new", [](const char *id_str, const char *json_str) -> vnode * {
json_error_t err;
uuid_t id;

uuid_parse(id_str, id);
auto *json = json_loads(json_str, 0, &err);

void *it = json_object_iter(json);
json_t *inner = json_object_iter_value(it);

if (json_is_object(inner)) { // create node with name
return (vnode *)villas::node::NodeFactory::make(json_object_iter_value(it), id, json_object_iter_key(it));
}
else { // create node without name
char* capi_str = json_dumps(json, 0);
auto ret = node_new(id_str, capi_str);

free(capi_str);
return ret;
}
},
py::return_value_policy::reference);

m.def("node_output_signals_max_cnt", [](void *n) -> unsigned {
return node_output_signals_max_cnt((vnode *)n);
});

m.def("node_pause", [](void *n) -> int {
return node_pause((vnode *)n);
});

m.def("node_poll_fds", [](void *n, int fds[]) -> int {
return node_poll_fds((vnode *)n, fds);
});

m.def("node_prepare", [](void *n) -> int {
return node_prepare((vsample *)n);
});

m.def("node_read", [](void *n, Array &a, unsigned cnt) -> int {
return node_read((vnode *)n, a.get_smps(), cnt);
});

m.def("node_restart", [](void *n) -> int {
return node_restart((vnode *)n);
});

m.def("node_resume", [](void *n) -> int {
return node_resume((vnode *)n);
});

m.def("node_reverse", [](void *n) -> int {
return node_reverse((vnode *)n);
});

m.def("node_start", [](void *n) -> int {
return node_start((vnode *)n);
});

m.def("node_stop", [](void *n) -> int {
return node_stop((vnode *)n);
});

m.def("node_to_json", [](void *n) -> py::str {
auto json = reinterpret_cast<villas::node::Node *>(n)->toJson();
char* json_str = json_dumps(json, 0);
auto py_str = py::str(json_str);

json_decref(json);
free(json_str);

return py_str;
});

m.def("node_to_json_str", [](void *n) -> py::str {
auto json = reinterpret_cast<villas::node::Node *>(n)->toJson();
char* json_str = json_dumps(json, 0);
auto py_str = py::str(json_str);

json_decref(json);
free(json_str);

return py_str;
});

m.def("node_write", [](void *n, Array &a, unsigned cnt) -> int {
return node_write((vnode *)n, a.get_smps(), cnt);
});

m.def("smps_array", [](unsigned int len) -> Array* {
return new Array(len);
},
py::return_value_policy::take_ownership);

m.def("sample_alloc", [](unsigned int len) -> vsample * {
return sample_alloc(len);
});

// Decrease reference count and release memory if last reference was held.
m.def("sample_decref", [](void *smps) -> void {
auto smp = (vsample **)smps;
sample_decref(*smp);
});

m.def("sample_length", [](void *smp) -> unsigned {
return sample_length((vsample *)smp);
});

m.def("sample_pack", &sample_pack, py::return_value_policy::reference);

m.def("sample_unpack", [](void *smp, unsigned *seq, struct timespec *ts_origin,
struct timespec *ts_received, int *flags, unsigned *len,
double *values) -> void {
return sample_unpack((vsample *)smp, seq, ts_origin, ts_received, flags, len, values);
},
py::return_value_policy::reference);

py::class_<Array>(m, "SamplesArray")
.def(py::init<unsigned int>(), py::arg("len"))
.def("__getitem__", [](Array &a, unsigned int idx) {
if (idx >= a.size()) {
throw py::index_error("Index out of bounds");
}
return a[idx];
})
.def("__setitem__", [](Array &a, unsigned int idx, void *smp) {
if (idx >= a.size()) {
throw py::index_error("Index out of bounds");
}
if (a[idx]) {
sample_decref(a[idx]);
}
a[idx] = (vsample *)smp;
});
}
2 changes: 1 addition & 1 deletion lib/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ Node *NodeFactory::make(json_t *json, const uuid_t &id,
std::string type;
Node *n;

if (json_is_object(json))
if (!json_is_object(json))
throw ConfigError(json, "node-config-node",
"Node configuration must be an object");

Expand Down
5 changes: 4 additions & 1 deletion packaging/docker/Dockerfile.debian
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ RUN apt-get update && \
liblua5.3-dev \
libhiredis-dev \
libnice-dev \
libmodbus-dev
libmodbus-dev \
python3 \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on python3 here is not required as its a direct dependency of python3-dev.
See: https://debian.pkgs.org/12/debian-main-arm64/python3-dev_3.11.2-1+b1_arm64.deb.html

E.g. we are also not depending on libmodbus, and only on libmodbus-dev.

python3-dev \
python3-pybind11

# Add local and 64-bit locations to linker paths
ENV echo /usr/local/lib >> /etc/ld.so.conf && \
Expand Down
5 changes: 4 additions & 1 deletion packaging/docker/Dockerfile.debian-multiarch
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ RUN apt-get update && \
libusb-1.0-0-dev:${ARCH} \
liblua5.3-dev:${ARCH} \
libhiredis-dev:${ARCH} \
libmodbus-dev:${ARCH}
libmodbus-dev:${ARCH} \
python3:${ARCH} \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some as above.

python3-dev:${ARCH} \
python3-pybind11:${ARCH}

# Add local and 64-bit locations to linker paths
ENV echo /usr/local/lib >> /etc/ld.so.conf && \
Expand Down
3 changes: 2 additions & 1 deletion packaging/docker/Dockerfile.fedora
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ RUN dnf -y install \
lua-devel \
hiredis-devel \
libnice-devel \
libmodbus-devel
libmodbus-devel \
pybind11-devel

# Add local and 64-bit locations to linker paths
RUN echo /usr/local/lib >> /etc/ld.so.conf && \
Expand Down
6 changes: 4 additions & 2 deletions packaging/docker/Dockerfile.rocky
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ RUN dnf --allowerasing -y install \
flex bison \
texinfo git curl tar \
protobuf-compiler protobuf-c-compiler \
clang-tools-extra
clang-tools-extra \
python python-devel

# Dependencies
RUN dnf -y install \
Expand All @@ -48,7 +49,8 @@ RUN dnf -y install \
lua-devel \
hiredis-devel \
libnice-devel \
libmodbus-devel
libmodbus-devel \
pybind11-devel

# Add local and 64-bit locations to linker paths
ENV echo /usr/local/lib >> /etc/ld.so.conf && \
Expand Down
5 changes: 4 additions & 1 deletion packaging/docker/Dockerfile.ubuntu
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ RUN apt-get update && \
libmodbus-dev \
libre2-dev \
libglib2.0-dev \
libcriterion-dev
libcriterion-dev \
python3 \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

python3-dev \
python3-pybind11

# Add local and 64-bit locations to linker paths
ENV echo /usr/local/lib >> /etc/ld.so.conf && \
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,14 @@ add_custom_target(run-unit-tests
USES_TERMINAL
)

find_package(Python3 REQUIRED COMPONENTS Interpreter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is needed? I dont see the variables/targets defined by find_package being used in this file.

Normally, such transitive dependencies should be defined using the PUBLIC keyword in the target_link_library, so they propagate downstream.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not necessary.
The idea behind it was to have CMake find the path of the Python interpreter.
I wanted to ensure that the Python executable was properly found, but just calling with python or python3 instead of ${PYTHON_EXECUTABLE} does the trick as well.

I ran into some permission issues during testing within in a local docker container and was worried that this could have caused the error, but it was an error during testing on my part.

add_custom_target(run-python-wrapper-tests
COMMAND
${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tests/unit/python_wrapper.py
DEPENDS
python-wrapper
USES_TERMINAL
)

add_dependencies(tests unit-tests)
add_dependencies(run-tests run-unit-tests)
add_dependencies(run-tests run-unit-tests run-python-wrapper-tests)
Loading