Add Python Function Blocks support#47
Conversation
This commit adds runtime support for Python Function Blocks, enabling PLC programs to include function blocks written in Python that communicate with the runtime via shared memory. Changes: - Add core/src/plc_app/include/iec_python.h: Header declaring Python FB loader functions (create_shm_name, python_block_loader) - Add core/src/plc_app/python_loader.c: Implementation ported from v3, adapted to use v4's logging API (log_info, log_error) - Update scripts/compile.sh: Include Python header in generated code compilation and link python_loader with pthread and rt libraries The Python FB loader creates shared memory regions for input/output data exchange and spawns Python processes that run the user's function block code. This matches the behavior of OpenPLC Runtime v3. Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
The python_loader.c was calling log_info() and log_error() directly, but these functions are defined in plc_main, not in libplc.so. Since plc_main is not linked with -rdynamic, these symbols would not be resolved at runtime. This commit implements Option B (function pointers/callbacks) to fix the issue: - Add static function pointers for logging in python_loader.c - Add fallback logging to stderr when loggers are not set - Add python_loader_set_loggers() function to inject logging callbacks - Update iec_python.h with the setter function declaration - Update symbols_init() in image_tables.c to wire up logging callbacks after loading libplc.so This approach matches the existing plugin system pattern where logging functions are passed via callbacks rather than relying on symbol export. Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
Per review feedback, since python_loader.c is always compiled into libplc.so and symbols_init() always runs before any Python FB code executes, the logging callbacks will always be set. Therefore, the fallback logging to stderr is unnecessary. Changes: - Remove fallback_log, py_log_info_fallback, py_log_error_fallback functions - Remove NULL checks in LOG_INFO/LOG_ERROR macros - Simplify macros to directly call the function pointers - Remove stdarg.h include (no longer needed) Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR adds runtime support for Python Function Blocks by enabling PLC programs to include Python-based function blocks that communicate with the runtime via POSIX shared memory. The implementation is ported from OpenPLC Runtime v3 and adapted to v4's architecture.
Key changes:
- Introduces shared memory-based communication mechanism for Python function blocks
- Implements function pointer-based logging to avoid symbol resolution issues between libplc.so and the main executable
- Updates the compilation process to link necessary libraries and force-include Python headers
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| core/src/plc_app/include/iec_python.h | Declares API functions for creating shared memory regions, loading Python blocks, and setting logging callbacks |
| core/src/plc_app/python_loader.c | Implements shared memory creation, Python process spawning, and thread-based output logging |
| core/src/plc_app/image_tables.c | Injects logging callbacks into python_loader after loading libplc.so |
| scripts/compile.sh | Updates compilation to force-include iec_python.h and link pthread/rt libraries |
Comments suppressed due to low confidence (1)
core/src/plc_app/include/iec_python.h:1
- The documentation states logging will fall back to stderr output if
python_loader_set_loggers()is not called, but according to the PR description, the fallback mechanism was removed. This comment should be updated to reflect that logging callbacks are always set bysymbols_init()before any Python FB code executes.
//-----------------------------------------------------------------------------
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| FILE *fp = fopen(script_name, "w"); | ||
| if (!fp) | ||
| { | ||
| LOG_ERROR("[Python loader] Failed to write Python script: %s", strerror(errno)); | ||
| return -1; | ||
| } | ||
| chmod(script_name, 0640); |
There was a problem hiding this comment.
File permissions are set after fopen but before writing content. If the file already exists, this chmod may fail or modify existing permissions unexpectedly. The chmod should occur after verifying the file was created successfully, or the file should be created with specific permissions using open() with O_CREAT and mode flags.
| FILE *fp = fopen(script_name, "w"); | |
| if (!fp) | |
| { | |
| LOG_ERROR("[Python loader] Failed to write Python script: %s", strerror(errno)); | |
| return -1; | |
| } | |
| chmod(script_name, 0640); | |
| int script_fd = open(script_name, O_WRONLY | O_CREAT | O_TRUNC, 0640); | |
| if (script_fd < 0) | |
| { | |
| LOG_ERROR("[Python loader] Failed to write Python script: %s", strerror(errno)); | |
| return -1; | |
| } | |
| FILE *fp = fdopen(script_fd, "w"); | |
| if (!fp) | |
| { | |
| LOG_ERROR("[Python loader] fdopen failed: %s", strerror(errno)); | |
| close(script_fd); | |
| return -1; | |
| } |
| char *cmd = malloc(512); | ||
| if (cmd == NULL) | ||
| { | ||
| LOG_ERROR("[Python loader] malloc failed for cmd buffer"); |
There was a problem hiding this comment.
If malloc fails, the function returns -1 without cleaning up the already-created shared memory regions and mappings. This causes resource leaks. The cleanup should unmap both shm_in_ptr and shm_out_ptr, and call shm_unlink for both shared memory regions before returning.
| LOG_ERROR("[Python loader] malloc failed for cmd buffer"); | |
| LOG_ERROR("[Python loader] malloc failed for cmd buffer"); | |
| munmap(*shm_in_ptr, shm_in_size); | |
| munmap(*shm_out_ptr, shm_out_size); | |
| shm_unlink(shm_in_name); | |
| shm_unlink(shm_out_name); |
| // Initialize Python loader logging callbacks (optional - only present if Python FBs are used) | ||
| void (*ext_python_loader_set_loggers)(void (*)(const char *, ...), void (*)(const char *, ...)); | ||
| *(void **)(&ext_python_loader_set_loggers) = | ||
| plugin_manager_get_func(pm, void (*)(unsigned long), "python_loader_set_loggers"); |
There was a problem hiding this comment.
The second argument to plugin_manager_get_func appears to be a function pointer type void (*)(unsigned long), but this doesn't match the actual signature of python_loader_set_loggers which takes two variadic function pointers. This type mismatch could cause incorrect function resolution or undefined behavior.
| plugin_manager_get_func(pm, void (*)(unsigned long), "python_loader_set_loggers"); | |
| plugin_manager_get_func(pm, void (*)(void (*)(const char *, ...), void (*)(const char *, ...)), "python_loader_set_loggers"); |
Summary
This PR adds runtime support for Python Function Blocks, enabling PLC programs to include function blocks written in Python that communicate with the runtime via shared memory. This is a port of the functionality from OpenPLC Runtime v3, adapted to v4's architecture.
Changes:
core/src/plc_app/include/iec_python.h- Header declaringcreate_shm_name(),python_block_loader(), andpython_loader_set_loggers()functions with C/C++ compatibilitycore/src/plc_app/python_loader.c- Implementation that creates POSIX shared memory regions and spawns Python processes, using function pointers for loggingcore/src/plc_app/image_tables.c- Updatedsymbols_init()to inject logging callbacks after loading libplc.soscripts/compile.sh- Updated to force-include the Python header when compiling generated code and link with-lpthread -lrtHow it works: When a PLC program contains a Python Function Block, the generated C code calls
python_block_loader()which writes the Python script to disk, creates shared memory regions for input/output data exchange, and spawns a detached thread runningpython3 -u <script>. The Python script reads inputs from shared memory, executes user logic, and writes outputs back.Updates since last revision
Fixed logging accessibility issue: Since
python_loader.cis compiled intolibplc.sobutlog_info()/log_error()are defined inplc_main(which is not linked with-rdynamic), direct calls would fail at runtime. The fix uses function pointers/callbacks:python_loader_set_loggers()to inject logging functions from the runtimesymbols_init()now resolves and calls the setter after loading libplc.soSimplified logging implementation per review feedback: Removed fallback-to-stderr mechanism since
python_loader.cis always compiled intolibplc.soandsymbols_init()always runs before any Python FB code executes, so logging callbacks are guaranteed to be set.Review & Testing Checklist for Human
Notes
OpenPLC_v3/webserver/core/python_loader.cpp-include iec_python.hflag injects the function declarations at compile timepython_loader.cis always compiled intolibplc.soregardless of whether the program uses Python FBsLink to Devin run: https://app.devin.ai/sessions/3b608658df0e4516ae7fd2d03792c4d1
Requested by: Thiago Alves (thiago.alves@autonomylogic.com) / @thiagoralves