Skip to content

C++ and Python Property Synchronization

This document describes how object properties instanced in C++ and Python are synchronized using the uLib property framework (uprops::Property<T>, uprops::PropertyContainer, and uprops::LocalSignalBackend).

graph LR
    subgraph C++ Runtime
        CppObj["C++ Object Instance<br/>(uprops::PropertyContainer)"]
        CppProp["Property<T><br/>(e.g., Speed, Status)"]
        CppLSB["uprops::LocalSignalBackend"]
        CppObj --> CppProp
        CppProp -->|Set() / PublishChange()| CppLSB
    end

    subgraph IPC Socket Bridge / Pybind11
        Bridge["Event Bus / TCP Socket Bridge<br/>(PropertyChangePayload JSON)"]
    end

    subgraph Python Runtime
        PyLSB["uLib_python.Core.LocalSignalBackend"]
        PySub["Python Callback / Subscriber"]
        PyObj["Python Object Proxy"]
        PySub --> PyObj
        PyLSB --> PySub
    end

    CppLSB <-->|Payload Notification| Bridge
    Bridge <-->|SubscribeCallback / PublishChange| PyLSB

1. Overview

In uLib, domain objects inherit from uprops::PropertyContainer and hold strongly typed properties (uprops::Property<T>). Whenever a property is updated via Set(), it publishes a PropertyChangePayload notification containing metadata: * propertyName: Short property identifier (e.g. "Speed"). * qualifiedName: Full property path (e.g. "Device.Speed"). * valueAsString: String representation of the updated value (e.g. "45.5"). * typeName: Type index string (e.g. "double").

Because uLib_python exposes LocalSignalBackend and PropertyContainer to Python via pybind11, applications can synchronize properties bidirectionally either in-process (embedded runtime) or inter-process (separate C++ daemon and Python client connected over TCP/IPC).


2. In-Process Synchronization Example

When Python imports uLib_python, both C++ and Python share the LocalSignalBackend singleton within the process.

C++ Code

#include "Core/uprops/PropertyContainer.h"
#include "Core/uprops/Property.h"
#include "Core/uprops/backend/LocalSignalBackend.h"

class SynchronizedDevice : public uprops::PropertyContainer {
public:
    SynchronizedDevice()
        : speed(this, "Speed", 10.0, "m/s", "Motion"),
          status(this, "Status", std::string("Idle"), "", "System") {}

    uprops::Property<double> speed;
    uprops::Property<std::string> status;
};

void ModifyFromCpp(SynchronizedDevice& dev) {
    // Changing property automatically emits PropertyChangePayload
    dev.speed.Set(85.5);
    dev.status.Set("Running");
}

Python Code

import uLib_python.Core as uCore

# 1. Subscribe to property change notifications
backend = uCore.LocalSignalBackend.Instance()

def on_property_changed(payload):
    print(f"[Python Received] Property '{payload.propertyName}' changed to '{payload.valueAsString}'")

subscriber = backend.SubscribeCallback(on_property_changed)

# 2. Publish property change from Python
payload = uCore.PropertyChangePayload()
payload.propertyName = "Status"
payload.qualifiedName = "Device.Status"
payload.valueAsString = "ConfiguredFromPython"
payload.typeName = "string"

backend.PublishChange(payload)

3. Inter-Process (Socket IPC) Synchronization Example

For standalone applications running in separate processes (a C++ server daemon and a Python control script), property changes are serialized to JSON over a TCP socket connection.

C++ App (cpp_py_sync_app.cpp)

The C++ application registers a LocalSignalBackend callback that forwards property updates to the connected Python client and receives incoming property updates to set on C++ object instances:

// Forwarding local C++ property changes to Python socket client
auto subscriber = uprops::LocalSignalBackend::Instance().SubscribeCallback(
    [clientSocket](const uprops::PropertyChangePayload& payload) {
        std::stringstream ss;
        ss << "{\"propertyName\":\"" << payload.propertyName
           << "\",\"valueAsString\":\"" << payload.valueAsString << "\"}\n";
        std::string json = ss.str();
        send(clientSocket, json.c_str(), json.length(), 0);
    }
);

// Applying incoming property change from Python to C++ object
uprops::PropertyBase* prop = device.GetProperty(propName);
if (prop && propName == "Status") {
    device.status.Set(valStr);
}

Python Script (cpp_py_sync_test.py)

Python connects to the C++ server over TCP, sends JSON property update commands, and receives real-time property change events published by C++:

import socket, json

# Connect to C++ server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9885))

# Send property update from Python to C++ object instance
update_payload = {
    "propertyName": "Status",
    "valueAsString": "RunningFromPython",
    "typeName": "string"
}
s.sendall((json.dumps(update_payload) + "\n").encode('utf-8'))

# Receive acknowledgment from C++
response = s.recv(1024).decode('utf-8')
print("[Python <- C++] Received ACK:", response)

4. Building and Running

Build the C++ Application and Python Bindings

micromamba run -n uLib cmake --preset clang-make
micromamba run -n uLib cmake --build build/clang-make --target cpp_py_sync_app uLib_python -j"$(nproc)"

Run Automated CTest Verification

micromamba run -n uLib ctest --test-dir build/clang-make -R pybind_cpp_py_sync --output-on-failure

Run Manually

  1. Launch C++ Server:
    ./build/clang-make/src/Python/testing/cpp_py_sync_app --port 9885
    
  2. In another terminal, execute Python client:
    PYTHONPATH=build/clang-make/src/Python:src/Python python src/Python/testing/cpp_py_sync_test.py