Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4791a15
refactor(image): one image symbol, and a tripwire for the heap move (…
JulioSergioFS Sep 9, 2026
00456cf
feat(webserver): install the I/O image sizes from the upload (RTOP-284)
JulioSergioFS Sep 9, 2026
229fe8a
feat(image): derive the image floor from the loaded program (RTOP-284)
JulioSergioFS Sep 9, 2026
2e835ee
feat(image): allocate the I/O image on program load (RTOP-284)
JulioSergioFS Sep 9, 2026
92d19b9
fix(journal): size the forced-slot map from the image (RTOP-284)
JulioSergioFS Sep 9, 2026
c2c00a8
fix(plugins): expose the range the image actually has (RTOP-284)
JulioSergioFS Sep 9, 2026
2ffc4da
fix(image): the derived floor was always zero, and three more blocker…
JulioSergioFS Sep 10, 2026
e8b463a
fix(image): finish the review's Required list (RTOP-284)
JulioSergioFS Sep 10, 2026
f87905c
feat(image): read the unit from image.conf, and convert where the sha…
JulioSergioFS Sep 11, 2026
a9c4906
feat(image): allocate each table at its own length, told to the plugins
JulioSergioFS Sep 14, 2026
41c202a
feat(image): let plugin_types.h name the image tables
JulioSergioFS Sep 14, 2026
fa5bbc3
fix(image): the image is the program's, and a failure has to stop the…
JulioSergioFS Sep 14, 2026
0569696
Merge branch 'RTOP-284-allocate-the-io-image-on-program-load' of http…
JulioSergioFS Sep 14, 2026
1e6a18c
fix(image): make the feature actually engage, and pin the three loose…
JulioSergioFS Sep 14, 2026
da21ef6
fix(image): the nits, and the bound now comes from where the pointers…
JulioSergioFS Sep 14, 2026
cbd4093
fix(journal): a refused force no longer shows as forced
JulioSergioFS Sep 14, 2026
13193f7
Merge branch 'RTOP-284-allocate-the-io-image-on-program-load' of http…
JulioSergioFS Sep 14, 2026
cb89382
fix(journal): take the bound from the same moment as the pointers
JulioSergioFS Sep 14, 2026
18ca4ce
Merge pull request #196 from Autonomy-Logic/RTOP-284-B-per-table-allo…
JulioSergioFS Sep 15, 2026
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
359 changes: 287 additions & 72 deletions core/src/drivers/plugin_driver.c

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions core/src/drivers/plugin_driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ typedef int (*plugin_retain_load_func_t)(const char *program_md5, uint16_t md5_l
* assumed to commit inside save(), which is where durability belongs anyway. */
typedef int (*plugin_retain_flush_func_t)(void);

/* Optional, and its PRESENCE is the capability declaration (RTOP-284).
*
* The fourteen image tables no longer share one length, and
* `plugin_runtime_args_t` carries a single `buffer_size` that cannot say so.
* Rather than move that struct -- CON06 guarantees pre-compiled plugins keep
* their field offsets -- the sizes travel through a symbol the loader resolves
* with dlsym, exactly as it already does for execute_command, get_stats and
* the three retain_* hooks.
*
* Exporting it means "I understand per-table sizes". A plugin without it is
* not broken and is not refused; the runtime keeps the image SQUARE for that
* run instead, because a plugin bounding a byte index and a word index with
* the same number is only safe while the tables are equal.
*
* Called BEFORE init(), once per plugin_driver_init(). Self-contained on
* purpose -- no args -- which is what lets it run that early. A non-zero
* return fails the plugin exactly as a failed init() does.
*
* `sizes` is indexed by `image_table_id_t` and `count` is how many entries it
* carries, so a plugin built against an older enum reads the prefix it knows
* and ignores the rest. */
typedef int (*plugin_set_image_sizes_func_t)(const uint32_t *sizes, uint32_t count);

typedef struct
{
void *handle; // Handle to the loaded shared library
Expand All @@ -97,6 +120,9 @@ typedef struct
plugin_retain_save_func_t retain_save;
plugin_retain_load_func_t retain_load;
plugin_retain_flush_func_t retain_flush;
/* Optional; NULL means this plugin does not understand per-table sizes and
* the image stays square for the run. */
plugin_set_image_sizes_func_t set_image_sizes;
} plugin_funct_bundle_t;

// Plugin instance structure
Expand Down Expand Up @@ -149,6 +175,22 @@ int plugin_driver_init(plugin_driver_t *driver);
* state and duplicates threads/sockets. Safe to call when no plugins are
* initialised. Returns the count of plugins it cleaned up. */
int plugin_driver_cleanup_init(plugin_driver_t *driver);

/**
* Is any plugin still initialised?
*
* Asked before releasing the image, which is the half of the ordering
* requirement that had no check. The allocate-before-init half is guarded by a
* real runtime refusal in generate_structured_args_with_driver; this is its
* counterpart, so a refactor that frees the image while a plugin still holds
* the base pointers it copied by value at init() gets a diagnostic instead of
* a use-after-free.
*
* Not the same as "still running": cleanup_init skips a plugin whose
* `initialized` is 0, and a native plugin with no `cleanup` symbol keeps its
* by-value args copy either way.
*/
int plugin_driver_any_initialized(plugin_driver_t *driver);
int plugin_driver_start(plugin_driver_t *driver);
int plugin_driver_stop(plugin_driver_t *driver);
void plugin_driver_destroy(plugin_driver_t *driver);
Expand Down Expand Up @@ -187,6 +229,26 @@ int plugin_driver_retain_load(plugin_instance_t *store, const char *program_md5,
uint8_t *out, uint16_t cap, uint16_t *out_len);
int plugin_driver_retain_flush(plugin_instance_t *store);

/**
* Does every loaded plugin understand per-table image sizes?
*
* Asked once per program load, BEFORE the image is allocated, because the
* answer decides how it is allocated. If any plugin says no, the image stays
* SQUARE for that run -- every table the same length.
*
* That is not a preference. The shipped VPP plugins bound a byte index into
* bool_output and a word index into int_output with the same `buffer_size`:
* the largest table would be an out-of-bounds read on the smaller ones, and
* the smallest would silently drop configured I/O. Only equal lengths keep one
* bound honest.
*
* `first_without` receives the name of the first plugin that does not, so the
* decision can be logged with a reason. It is the only way an operator can
* tell the two modes apart.
*/
bool plugin_driver_all_understand_per_table_sizes(plugin_driver_t *driver,
const char **first_without);

// Route a command to a specific plugin by name (for async commands like scan)
int plugin_driver_execute_command(plugin_driver_t *driver, const char *plugin_name,
const char *command_json, char *response, size_t response_size);
Expand Down
41 changes: 40 additions & 1 deletion core/src/drivers/plugin_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
#define PLUGIN_TYPES_H

#include "../lib/iec_types.h"
/* The image table identities, so a plugin receiving the sizes array can name
* the entries it indexes rather than counting positions (RTOP-284, B2).
* Publishing a type costs no ABI: no struct gains a field and no offset
* moves, which is what CON06 guarantees pre-compiled plugins.
*
* A plugin built against an OLDER runtime will not find this header, so
* anything that must work on both — a VPP package, which ships and is built
* independently of the runtime on the device — carries its own constants and
* keeps them in the order documented there. */
#include "../plc_app/image_table_id.h"
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
Expand Down Expand Up @@ -229,7 +239,36 @@ typedef struct
/* Plugin configuration */
char plugin_specific_config_file_path[256];

/* Buffer size information */
/* THE SMALLEST TABLE, NOT THE ONLY ONE (RTOP-284).
*
* The fourteen image tables no longer share a length. This field cannot
* say that -- CON06 guarantees pre-compiled plugins keep their field
* offsets, so it does not move -- and it is now the MINIMUM of the
* fourteen rather than the length they all happened to have.
*
* The minimum is the only safe answer for a consumer that still reads one
* number: bounding by it refuses an index that would have run off the end
* of the shortest table, where bounding by the largest would have read
* past every table below it. Under-permissive, never over.
*
* A plugin that wants the truth exports `set_image_sizes` (plugin_driver.h)
* and receives all fourteen before its init() runs. When every loaded
* plugin does, the image is allocated per table; when any does not, it is
* kept square for that run and this field is again the length they all
* have.
*
* Not marked deprecated yet, and the reason is not what an earlier draft
* of this comment claimed. The runtime core does build with -Werror
* (core/src/CMakeLists.txt), but the plugins do not: they are configured
* by their own cmake invocation and the VPP packages by a plain Makefile,
* so the attribute would produce warnings there, not a build failure.
*
* It is deferred because the field is still the RIGHT thing to read: on a
* square run it is the length every table has, and it is the only bound a
* plugin that has not adopted set_image_sizes can use. Deprecating it now
* would warn at correct code, including in packages that ship
* independently and must keep working against older runtimes. The
* attribute goes in once the symbol is universal. */
int buffer_size;
int bits_per_buffer;

Expand Down
4 changes: 4 additions & 0 deletions core/src/drivers/plugins/native/ethercat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ set(PLUGIN_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/ethercat_proc.c
${CMAKE_CURRENT_SOURCE_DIR}/ethercat_iface_state.c
${OPENPLC_ROOT}/core/src/drivers/plugins/native/plugin_logger.c
# Exports set_image_sizes, which is how this plugin declares it
# understands per-table image sizes (RTOP-284). Without it the
# runtime keeps the image square for every run this plugin is in.
${OPENPLC_ROOT}/core/src/drivers/plugins/native/plugin_image_sizes.c
)

# =============================================================================
Expand Down
57 changes: 51 additions & 6 deletions core/src/drivers/plugins/native/ethercat/ethercat_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/

#include "ethercat_io.h"

#include "../plugin_image_sizes.h"
#include "ethercat_master.h"

#include <stdlib.h>
Expand Down Expand Up @@ -234,6 +236,31 @@ static int ecat_data_type_expected_iec_size(ecat_data_type_t dt)
/**
* @brief Return a human-readable name for an iec_size_t value
*/
/* (direction, size) -> the image table that stores it.
*
* EtherCAT only ever emits %I and %Q, so there is no memory case to answer.
* The mapping is spelled out rather than arithmetic on the enums: the two
* orders are unrelated and a cast between them would land writes in another
* table's bounds. */
static image_table_id_t ecat_table_for(iec_dir_t dir, iec_size_t size)
{
const int in = (dir == IEC_DIR_INPUT);
switch (size)
{
case IEC_SIZE_BIT:
return in ? IMAGE_TABLE_BOOL_INPUT : IMAGE_TABLE_BOOL_OUTPUT;
case IEC_SIZE_BYTE:
return in ? IMAGE_TABLE_BYTE_INPUT : IMAGE_TABLE_BYTE_OUTPUT;
case IEC_SIZE_WORD:
return in ? IMAGE_TABLE_INT_INPUT : IMAGE_TABLE_INT_OUTPUT;
case IEC_SIZE_DWORD:
return in ? IMAGE_TABLE_DINT_INPUT : IMAGE_TABLE_DINT_OUTPUT;
case IEC_SIZE_LWORD:
return in ? IMAGE_TABLE_LINT_INPUT : IMAGE_TABLE_LINT_OUTPUT;
}
return IMAGE_TABLE_COUNT;
}

static const char *iec_size_name(iec_size_t sz)
{
switch (sz) {
Expand Down Expand Up @@ -301,13 +328,31 @@ int ecat_io_build_channel_map(const ecat_config_t *config,
continue;
}

/* Bounds check against PLC buffer size */
if (iec_loc.byte_index >= args->buffer_size) {
/* Bounds check against THIS LOCATION'S OWN TABLE.
*
* args->buffer_size is now the SMALLEST of the fourteen, so using
* it here would refuse every channel above the shortest table's
* length -- an EtherCAT slave silently losing most of its I/O on a
* project that sizes one area small. The table the location
* actually lands in is the only honest bound (RTOP-284). */
const image_table_id_t table = ecat_table_for(iec_loc.direction, iec_loc.size);
/* Falls back to args->buffer_size when the sizes were never
* delivered. That is not belt and braces: without it a runtime
* that does not call set_image_sizes -- an older one loading this
* plugin -- leaves every table at zero here and EVERY channel is
* refused, taking the whole bus down silently. On a square run
* buffer_size IS the length every table has, so it is the right
* answer rather than a guess. */
const uint32_t reach = plugin_image_sizes_known()
? plugin_image_table_capacity(table)
: (uint32_t)(args->buffer_size > 0 ? args->buffer_size : 0);
if (iec_loc.byte_index < 0 || (uint32_t)iec_loc.byte_index >= reach)
{
plugin_logger_warn(logger,
"Slave '%s' channel '%s': IEC location '%s' byte index %d "
"exceeds buffer size %d, skipping",
cfg_slave->name, ch->name, ch->iec_location,
iec_loc.byte_index, args->buffer_size);
"Slave '%s' channel '%s': IEC location '%s' byte index %d "
"is outside the %u element(s) that area has, skipping",
cfg_slave->name, ch->name, ch->iec_location, iec_loc.byte_index,
reach);
errors++;
continue;
}
Expand Down
20 changes: 16 additions & 4 deletions core/src/drivers/plugins/native/ethercat/ethercat_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@
#include "plugin_logger.h"
#include "plugin_types.h"
#include "ethercat_plugin.h"

#include "../plugin_image_sizes.h"
#include "cJSON.h" /* JSON parsing for execute_command */
#include "ethercat_config.h"
#include "ethercat_master.h"
#include "ethercat_io.h"
#include "soem/soem.h" /* osal_get_monotonic_time, ec_timet */
#include "cJSON.h" /* JSON parsing for execute_command */
#include "ethercat_master.h"
#include "soem/soem.h" /* osal_get_monotonic_time, ec_timet */

/* Forward declaration: ecat_bus_thread is defined alongside the bus
* loop further down in the file but referenced first by
Expand Down Expand Up @@ -1198,7 +1200,17 @@ int init(void *args)
* land in the runtime journal instead of stderr. */
ecat_config_set_logger(&g_logger);

plugin_logger_info(&g_logger, "Buffer size: %d", g_runtime_args.buffer_size);
/* What this plugin was told, not the deprecated single figure: on a
* per-table run buffer_size is the SMALLEST of the fourteen and says
* nothing about the areas this bus actually reaches. */
if (!plugin_image_sizes_known())
plugin_logger_info(&g_logger, "Image sizes: not delivered (square run)");
else
plugin_logger_info(&g_logger, "Image sizes: bits in %u/%u, words in %u/%u",
plugin_image_table_capacity(IMAGE_TABLE_BOOL_INPUT),
plugin_image_table_capacity(IMAGE_TABLE_BOOL_OUTPUT),
plugin_image_table_capacity(IMAGE_TABLE_INT_INPUT),
plugin_image_table_capacity(IMAGE_TABLE_INT_OUTPUT));

/* Parse ALL master configurations from the JSON file */
const char *config_path = g_runtime_args.plugin_specific_config_file_path;
Expand Down
37 changes: 37 additions & 0 deletions core/src/drivers/plugins/native/plugin_image_sizes.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "plugin_image_sizes.h"

#include <string.h>

/* Zeroed until the runtime calls set_image_sizes(), which it does once per
* plugin_driver_init() and therefore once per program load. Deliberately NOT
* remembered across loads: a cached copy from the previous program is exactly
* the stale state the per-load delivery exists to prevent. */
static uint32_t g_sizes[IMAGE_TABLE_COUNT];
static int g_known = 0;

/**
* Exported for the runtime to find by dlsym. Its presence is the declaration.
*
* `count` is how many entries the runtime sent, which need not be
* IMAGE_TABLE_COUNT: a plugin built against an older enum reads the prefix it
* knows and ignores the rest, and one built against a newer enum leaves the
* tail at zero rather than reading past the array.
*/
int set_image_sizes(const uint32_t *sizes, uint32_t count)
{
memset(g_sizes, 0, sizeof(g_sizes));
if (!sizes) return -1;

const uint32_t n = count < (uint32_t)IMAGE_TABLE_COUNT ? count : (uint32_t)IMAGE_TABLE_COUNT;
for (uint32_t i = 0; i < n; ++i) g_sizes[i] = sizes[i];
g_known = 1;
return 0;
}

uint32_t plugin_image_table_capacity(image_table_id_t id)
{
if (id < 0 || id >= IMAGE_TABLE_COUNT) return 0;
return g_sizes[id];
}

int plugin_image_sizes_known(void) { return g_known; }
46 changes: 46 additions & 0 deletions core/src/drivers/plugins/native/plugin_image_sizes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* The fourteen image table lengths, for a native plugin (RTOP-284).
*
* Linking this file into a plugin gives it two things at once: the exported
* `set_image_sizes` symbol the runtime looks for -- whose PRESENCE is how a
* plugin declares it understands per-table sizes -- and the accessor to read
* back what it was told.
*
* One implementation rather than one per plugin, for the same reason the
* logger is shared: three copies of a fourteen-element cache is three places
* for the indexing to drift, and the whole point of this work is that the
* tables no longer share a length.
*
* A plugin that does NOT link this is not broken. The runtime keeps the image
* square for that run and says which plugin forced it.
*/

#ifndef PLUGIN_IMAGE_SIZES_H
#define PLUGIN_IMAGE_SIZES_H

#include "../../../plc_app/image_table_id.h"

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
* How long one table is, in its own elements.
*
* Returns 0 before the runtime has delivered the sizes and for an id this
* build does not know, which are the same answer for a caller: an area it
* cannot index into. Bounding against 0 refuses every access, which is the
* safe direction for a plugin asked to act before it has been told anything.
*/
uint32_t plugin_image_table_capacity(image_table_id_t id);

/** Whether the runtime has delivered the sizes for this load yet. */
int plugin_image_sizes_known(void);

#ifdef __cplusplus
}
#endif

#endif /* PLUGIN_IMAGE_SIZES_H */
4 changes: 4 additions & 0 deletions core/src/drivers/plugins/native/s7comm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ set(PLUGIN_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/s7comm_plugin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/s7comm_config.c
${OPENPLC_ROOT}/core/src/drivers/plugins/native/plugin_logger.c
# Exports set_image_sizes, which is how this plugin declares it
# understands per-table image sizes (RTOP-284). Without it the
# runtime keeps the image square for every run this plugin is in.
${OPENPLC_ROOT}/core/src/drivers/plugins/native/plugin_image_sizes.c
)

# =============================================================================
Expand Down
Loading
Loading