-
Notifications
You must be signed in to change notification settings - Fork 85
feat(image): allocate each table at its own length, told to the plugins #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9c4906
41c202a
0569696
1e6a18c
da21ef6
13193f7
cb89382
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -591,6 +591,122 @@ int plugin_driver_load_config(plugin_driver_t *driver, const char *config_file) | |
| } | ||
|
|
||
| // Send to plugin init function all args | ||
| /** | ||
| * The fourteen table lengths, for a plugin that asked to be told. | ||
| * | ||
| * Read once per plugin_driver_init() rather than cached: the image is | ||
| * reallocated on every program load, and a cached copy is exactly the stale | ||
| * state this symbol exists to prevent. | ||
| */ | ||
| static uint32_t image_sizes_snapshot(uint32_t *out, uint32_t cap) | ||
| { | ||
| uint32_t n = 0; | ||
| for (int id = 0; id < IMAGE_TABLE_COUNT && n < cap; ++id) | ||
| { | ||
| out[n++] = image_table_capacity((image_table_id_t)id); | ||
| } | ||
| return n; | ||
| } | ||
|
|
||
| /** | ||
| * Hand one plugin the fourteen lengths, before its init() runs. | ||
| * | ||
| * Returns 0 when the plugin has nothing to be told or accepted them, and | ||
| * non-zero when it refused -- which fails the plugin exactly as a failed | ||
| * init() does, because a plugin that cannot make sense of the image it is | ||
| * about to be handed should not be handed it. | ||
| */ | ||
| static int deliver_image_sizes(plugin_instance_t *plugin) | ||
| { | ||
| uint32_t sizes[IMAGE_TABLE_COUNT]; | ||
| const uint32_t count = image_sizes_snapshot(sizes, IMAGE_TABLE_COUNT); | ||
|
|
||
| if (plugin->config.type == PLUGIN_TYPE_NATIVE && plugin->native_plugin && | ||
| plugin->native_plugin->set_image_sizes) | ||
| { | ||
| const int rc = plugin->native_plugin->set_image_sizes(sizes, count); | ||
| if (rc != 0) | ||
| { | ||
| log_error("Plugin '%s' refused the image sizes (returned %d)", plugin->config.name, rc); | ||
| return rc; | ||
| } | ||
| } | ||
|
|
||
| if (plugin->config.type == PLUGIN_TYPE_PYTHON && plugin->python_plugin && | ||
| plugin->python_plugin->pFuncSetImageSizes) | ||
| { | ||
| PyObject *list = PyList_New((Py_ssize_t)count); | ||
| if (!list) | ||
| { | ||
| PyErr_Clear(); | ||
| log_error("Plugin '%s': could not build the image size list", plugin->config.name); | ||
| return -1; | ||
| } | ||
| for (uint32_t i = 0; i < count; ++i) | ||
| { | ||
| /* PyList_SetItem steals the reference, so a failed PyLong_FromLong | ||
| * is the only leak to worry about and it cannot happen for a | ||
| * uint32_t that already exists. */ | ||
| PyList_SetItem(list, (Py_ssize_t)i, PyLong_FromUnsignedLong(sizes[i])); | ||
| } | ||
| PyObject *result = | ||
| PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncSetImageSizes, list, NULL); | ||
| Py_DECREF(list); | ||
| if (!result) | ||
| { | ||
| PyErr_Print(); | ||
| log_error("Plugin '%s' raised in set_image_sizes", plugin->config.name); | ||
| return -1; | ||
| } | ||
| Py_DECREF(result); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| bool plugin_driver_all_understand_per_table_sizes(plugin_driver_t *driver, | ||
| const char **first_without) | ||
| { | ||
| if (first_without) | ||
| *first_without = NULL; | ||
| if (!driver) | ||
| return false; | ||
|
|
||
| for (int i = 0; i < driver->plugin_count; i++) | ||
| { | ||
| plugin_instance_t *plugin = &driver->plugins[i]; | ||
| bool understands = false; | ||
|
|
||
| /* A DEGRADED PLUGIN DOES NOT GET A VOTE. | ||
| * | ||
| * It failed to load, so plugin_driver_init skips it in every branch: | ||
| * it never receives runtime args and never touches the image. Letting | ||
| * it answer "no" would mean one box missing Npcap, where the EtherCAT | ||
| * plugin degrades, silently costs every OTHER plugin its per-table | ||
| * image -- a downgrade with no relation to anything that will actually | ||
| * read the tables. | ||
| * | ||
| * DISABLED plugins still vote, deliberately: plugin_driver_init | ||
| * initialises them regardless of the enabled flag, so a disabled | ||
| * plugin does hold the base pointers and does read the image. */ | ||
| if (plugin->degraded) | ||
| continue; | ||
|
|
||
| if (plugin->config.type == PLUGIN_TYPE_NATIVE) | ||
| understands = plugin->native_plugin && plugin->native_plugin->set_image_sizes; | ||
| else if (plugin->config.type == PLUGIN_TYPE_PYTHON) | ||
| understands = plugin->python_plugin && plugin->python_plugin->pFuncSetImageSizes; | ||
|
|
||
| if (!understands) | ||
| { | ||
| if (first_without) | ||
| *first_without = plugin->config.name; | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| int plugin_driver_init(plugin_driver_t *driver) | ||
| { | ||
| if (!driver) | ||
|
|
@@ -632,6 +748,18 @@ int plugin_driver_init(plugin_driver_t *driver) | |
| } | ||
| return -1; | ||
| } | ||
| /* BEFORE init(), because init() is where a plugin copies the | ||
| * base pointers by value and decides how big everything is. | ||
| * Delivering afterwards leaves a window, once per load, in which | ||
| * the plugin holds new pointers and previous sizes. */ | ||
| if (deliver_image_sizes(plugin) != 0) | ||
| { | ||
| Py_DECREF(args); | ||
| if (have_gil) | ||
| PyGILState_Release(local_gstate); | ||
| return -1; | ||
| } | ||
|
|
||
| // Call the Python init function with proper capsule | ||
| PyObject *result = | ||
| PyObject_CallFunctionObjArgs(plugin->python_plugin->pFuncInit, args, NULL); | ||
|
|
@@ -671,6 +799,15 @@ int plugin_driver_init(plugin_driver_t *driver) | |
| return -1; | ||
| } | ||
|
|
||
| /* BEFORE init(), for the same reason as the Python path above. */ | ||
| if (deliver_image_sizes(plugin) != 0) | ||
| { | ||
| free_structured_args(args); | ||
| if (have_gil) | ||
| PyGILState_Release(local_gstate); | ||
| return -1; | ||
| } | ||
|
|
||
| // Call the native init function | ||
| int result = plugin->native_plugin->init(args); | ||
| if (result != 0) | ||
|
|
@@ -1120,12 +1257,19 @@ void *generate_structured_args_with_driver(plugin_type_t type, plugin_driver_t * | |
| sizeof(driver->plugins[plugin_index].config.plugin_related_config_path)); | ||
|
|
||
| // Initialize buffer size info | ||
| /* The allocated size, not a compile-time constant. Plugins bounds-check | ||
| * against this field -- ethercat_io.c refuses a byte_index at or above it, | ||
| * s7comm derives every clamp from it -- so it has to describe the image | ||
| * that actually exists. It describes all fourteen tables because they are | ||
| * all allocated at the same count; see image_sizes_largest() for why the | ||
| * ABI leaves no room for anything else. */ | ||
| /* THE SMALLEST OF THE FOURTEEN, not the length they all share. | ||
| * | ||
| * The tables no longer have one length, and this field cannot say so -- | ||
| * CON06 keeps the struct's offsets fixed. The minimum is the only safe | ||
| * single number for a consumer that has not been told they can differ: | ||
| * bounding by it refuses an index, where bounding by the largest reads | ||
| * past every shorter table. | ||
| * | ||
| * ethercat_io.c and s7comm no longer derive their clamps from this field; | ||
| * they export set_image_sizes and bound by the table each access actually | ||
| * addresses, falling back here only when the sizes were never delivered. | ||
| * plugin_types.h carries the same statement for plugin authors, and | ||
| * journal_buffer.h for the runtime's own copy. */ | ||
| args->buffer_size = (int)image_tables_capacity(); | ||
| args->bits_per_buffer = 8; | ||
|
|
||
|
|
@@ -1335,6 +1479,10 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) | |
| // start_loop is optional | ||
| Py_XDECREF(py_binds->pFuncStart); | ||
| py_binds->pFuncStart = NULL; | ||
| /* A failed PyObject_GetAttrString leaves an AttributeError SET, and an | ||
| * optional lookup does not return, so it has to be cleared here or the | ||
| * next CPython call reports this absence as its own failure. */ | ||
| PyErr_Clear(); | ||
| } | ||
|
|
||
| py_binds->pFuncStop = PyObject_GetAttrString(py_binds->pModule, "stop_loop"); | ||
|
|
@@ -1343,6 +1491,23 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) | |
| // stop_loop is optional | ||
| Py_XDECREF(py_binds->pFuncStop); | ||
| py_binds->pFuncStop = NULL; | ||
| /* A failed PyObject_GetAttrString leaves an AttributeError SET, and an | ||
| * optional lookup does not return, so it has to be cleared here or the | ||
| * next CPython call reports this absence as its own failure. */ | ||
| PyErr_Clear(); | ||
| } | ||
|
|
||
| py_binds->pFuncSetImageSizes = PyObject_GetAttrString(py_binds->pModule, "set_image_sizes"); | ||
| if (!py_binds->pFuncSetImageSizes || !PyCallable_Check(py_binds->pFuncSetImageSizes)) | ||
| { | ||
| /* Optional. PyErr_Clear() is not decoration: a failed | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Nit · confidence 8/10 · the The block itself is correct and correctly positioned. But the comment says "the four required lookups above never hit it because they return on failure", and only one lookup above is required: So for a plugin that defines Either add |
||
| * PyObject_GetAttrString leaves an AttributeError SET, and the next | ||
| * CPython call that checks would report this absence as its own | ||
| * failure. The four required lookups above never hit it because they | ||
| * return on failure. */ | ||
| Py_XDECREF(py_binds->pFuncSetImageSizes); | ||
| py_binds->pFuncSetImageSizes = NULL; | ||
| PyErr_Clear(); | ||
| } | ||
|
|
||
| py_binds->pFuncCleanup = PyObject_GetAttrString(py_binds->pModule, "cleanup"); | ||
|
|
@@ -1351,6 +1516,10 @@ int python_plugin_get_symbols(plugin_instance_t *plugin) | |
| // cleanup is optional | ||
| Py_XDECREF(py_binds->pFuncCleanup); | ||
| py_binds->pFuncCleanup = NULL; | ||
| /* A failed PyObject_GetAttrString leaves an AttributeError SET, and an | ||
| * optional lookup does not return, so it has to be cleared here or the | ||
| * next CPython call reports this absence as its own failure. */ | ||
| PyErr_Clear(); | ||
| } | ||
|
|
||
| // Store the python binds in the plugin instance | ||
|
|
@@ -1487,6 +1656,11 @@ int native_plugin_get_symbols(plugin_instance_t *plugin) | |
| native_bundle->retain_load = (plugin_retain_load_func_t)dlsym(handle, "retain_load"); | ||
| native_bundle->retain_flush = (plugin_retain_flush_func_t)dlsym(handle, "retain_flush"); | ||
|
|
||
| /* Optional, like the five above: NULL simply means this plugin does not | ||
| * understand per-table image sizes, and the run stays square for it. */ | ||
| native_bundle->set_image_sizes = | ||
| (plugin_set_image_sizes_func_t)dlsym(handle, "set_image_sizes"); | ||
|
|
||
| // Store the native bundle and handle in the plugin instance | ||
| plugin->native_plugin = native_bundle; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Major · confidence 9/10 · the feature is inert on a stock device
This loop returns false on the first plugin without the symbol, and
plugins_default.confships five:grep -rl set_image_sizes core/src/drivers/plugins/python/matches exactly two files: the newshared/image_sizes.pyandmodbus_slave/simple_modbus.py. Neithermodbus_master_plugin.pynoropcua/plugin.pyimports it, sounderstandsis false for them,image_sizes_flattenruns on every load, and the run is square on any device using the shipped config.Per-table sizing therefore never activates as shipped. The two enum-order fixes, the three loop fixes and the s7comm/EtherCAT clamps all run in the degenerate case where they cannot differ from the old behaviour — which also means none of them is being exercised by anything.
The fix looks like one line each, and nothing else has to change: both plugins already bound through
SafeBufferAccess→BufferValidator.validate_buffer_access, which this PR already migrated totable_capacity(buffer_type, self.args.buffer_size), and the names inbuffer_types.pymatchIMAGE_TABLE_ORDERexactly.If that is deliberately out of scope for group B, then the PR description should say the feature ships off by default — as written it reads as though it ships on.
Related, and cheap to fix while you are here (🟢 Nit, confidence 7/10): a degraded plugin also costs the run its per-table image.
update_config/append_configleavenative_plugin == NULLand setdegraded = 1, so the plugin is skipped by every branch ofplugin_driver_init, never receives runtime args and never touches the image — yet it makesunderstandsfalse. On a box missing Npcap the EtherCAT plugin degrades and silently downgrades everyone else. Including disabled plugins is right, sinceplugin_driver_initinits them regardless ofconfig.enabled; a degraded one is a different case.if (plugin->degraded) continue;, or at minimum log the distinction so an operator can tell "this plugin is old" from "this plugin failed to load".