Skip to content

Add Python Function Blocks support#47

Merged
thiagoralves merged 3 commits into
developmentfrom
devin/1765329600-python-function-blocks
Dec 10, 2025
Merged

Add Python Function Blocks support#47
thiagoralves merged 3 commits into
developmentfrom
devin/1765329600-python-function-blocks

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Dec 10, 2025

Copy link
Copy Markdown
Contributor

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 declaring create_shm_name(), python_block_loader(), and python_loader_set_loggers() functions with C/C++ compatibility
  • core/src/plc_app/python_loader.c - Implementation that creates POSIX shared memory regions and spawns Python processes, using function pointers for logging
  • core/src/plc_app/image_tables.c - Updated symbols_init() to inject logging callbacks after loading libplc.so
  • scripts/compile.sh - Updated to force-include the Python header when compiling generated code and link with -lpthread -lrt

How 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 running python3 -u <script>. The Python script reads inputs from shared memory, executes user logic, and writes outputs back.

Updates since last revision

  1. Fixed logging accessibility issue: Since python_loader.c is compiled into libplc.so but log_info()/log_error() are defined in plc_main (which is not linked with -rdynamic), direct calls would fail at runtime. The fix uses function pointers/callbacks:

    • Added python_loader_set_loggers() to inject logging functions from the runtime
    • symbols_init() now resolves and calls the setter after loading libplc.so
    • This matches the existing plugin system pattern for passing logging callbacks
  2. Simplified logging implementation per review feedback: Removed fallback-to-stderr mechanism since python_loader.c is always compiled into libplc.so and symbols_init() always runs before any Python FB code executes, so logging callbacks are guaranteed to be set.

Review & Testing Checklist for Human

  • End-to-end test required: Create a PLC program with a Python Function Block in openplc-editor, upload to runtime v4, and verify the Python FB executes correctly and logs appear in the runtime output
  • Verify compile.sh works: Upload a regular PLC program (without Python FBs) to ensure the new compile flags don't break normal compilation
  • Review shared memory cleanup: The code creates shared memory regions but doesn't explicitly clean them up when PLC stops - verify if this is acceptable or if cleanup is needed in the shutdown path

Notes

  • The implementation was ported from OpenPLC_v3/webserver/core/python_loader.cpp
  • No openplc-editor changes are required - the -include iec_python.h flag injects the function declarations at compile time
  • Builds locally and CI passes, but not runtime-tested due to need for editor integration
  • python_loader.c is always compiled into libplc.so regardless of whether the program uses Python FBs

Link to Devin run: https://app.devin.ai/sessions/3b608658df0e4516ae7fd2d03792c4d1
Requested by: Thiago Alves (thiago.alves@autonomylogic.com) / @thiagoralves

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-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration Bot and others added 2 commits December 10, 2025 13:01
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>
@thiagoralves thiagoralves requested a review from Copilot December 10, 2025 18:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 by symbols_init() before any Python FB code executes.
//-----------------------------------------------------------------------------

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +119 to +125
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);

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
char *cmd = malloc(512);
if (cmd == NULL)
{
LOG_ERROR("[Python loader] malloc failed for cmd buffer");

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
// 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");

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

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.

Suggested change
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");

Copilot uses AI. Check for mistakes.
@thiagoralves thiagoralves merged commit 4521647 into development Dec 10, 2025
1 check passed
@thiagoralves thiagoralves deleted the devin/1765329600-python-function-blocks branch December 10, 2025 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants