-
Notifications
You must be signed in to change notification settings - Fork 50
Add Python Function Blocks support #47
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
2b1a81b
c1a7688
f72489e
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 |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| //----------------------------------------------------------------------------- | ||
| // Copyright 2025 Thiago Alves | ||
| // This file is part of the OpenPLC Runtime. | ||
| // | ||
| // OpenPLC is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // OpenPLC is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU General Public License | ||
| // along with OpenPLC. If not, see <http://www.gnu.org/licenses/>. | ||
| //------ | ||
| // | ||
| // This header declares the Python Function Block loader functions. | ||
| // These functions are used by the generated PLC code to load and execute | ||
| // Python function blocks via shared memory communication. | ||
| // | ||
| // Thiago Alves, Dec 2025 | ||
| //----------------------------------------------------------------------------- | ||
|
|
||
| #ifndef IEC_PYTHON_H | ||
| #define IEC_PYTHON_H | ||
|
|
||
| #include <stddef.h> | ||
| #include <sys/types.h> | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" | ||
| { | ||
| #endif | ||
|
|
||
| /** | ||
| * @brief Create a unique shared memory name | ||
| * | ||
| * Creates a unique name for shared memory regions using mkstemp. | ||
| * The name is suitable for use with shm_open(). | ||
| * | ||
| * @param buf Buffer to store the generated name | ||
| * @param size Size of the buffer | ||
| * @return 0 on success, -1 on failure | ||
| */ | ||
| int create_shm_name(char *buf, size_t size); | ||
|
|
||
| /** | ||
| * @brief Load and start a Python function block | ||
| * | ||
| * Writes the Python script to disk, creates shared memory regions for | ||
| * input/output data exchange, and spawns a Python process to execute | ||
| * the function block. | ||
| * | ||
| * @param script_name Path where the Python script will be written | ||
| * @param script_content The Python script content (with format specifiers for pid and shm_name) | ||
| * @param shm_name Base name for shared memory regions | ||
| * @param shm_in_size Size of the input shared memory region | ||
| * @param shm_out_size Size of the output shared memory region | ||
| * @param shm_in_ptr Pointer to store the mapped input shared memory address | ||
| * @param shm_out_ptr Pointer to store the mapped output shared memory address | ||
| * @param pid PLC process ID (passed to Python script for monitoring) | ||
| * @return 0 on success, -1 on failure | ||
| */ | ||
| int python_block_loader(const char *script_name, const char *script_content, char *shm_name, | ||
| size_t shm_in_size, size_t shm_out_size, void **shm_in_ptr, | ||
| void **shm_out_ptr, pid_t pid); | ||
|
|
||
| /** | ||
| * @brief Set logging function pointers for the Python loader | ||
| * | ||
| * This function must be called after loading libplc.so to inject the | ||
| * runtime's logging functions. Without this, logging will fall back | ||
| * to stderr output. | ||
| * | ||
| * @param log_info_func Pointer to the log_info function | ||
| * @param log_error_func Pointer to the log_error function | ||
| */ | ||
| void python_loader_set_loggers(void (*log_info_func)(const char *, ...), | ||
| void (*log_error_func)(const char *, ...)); | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif | ||
|
|
||
| #endif /* IEC_PYTHON_H */ |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,215 @@ | ||||||||||||||||||||||||||||||||||||||||||
| //----------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||
| // Copyright 2025 Thiago Alves | ||||||||||||||||||||||||||||||||||||||||||
| // This file is part of the OpenPLC Runtime. | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // OpenPLC is free software: you can redistribute it and/or modify | ||||||||||||||||||||||||||||||||||||||||||
| // it under the terms of the GNU General Public License as published by | ||||||||||||||||||||||||||||||||||||||||||
| // the Free Software Foundation, either version 3 of the License, or | ||||||||||||||||||||||||||||||||||||||||||
| // (at your option) any later version. | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // OpenPLC is distributed in the hope that it will be useful, | ||||||||||||||||||||||||||||||||||||||||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||||||||||||||||||||||||||||||||||||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||||||||||||||||||||||||||||||||||||||
| // GNU General Public License for more details. | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // You should have received a copy of the GNU General Public License | ||||||||||||||||||||||||||||||||||||||||||
| // along with OpenPLC. If not, see <http://www.gnu.org/licenses/>. | ||||||||||||||||||||||||||||||||||||||||||
| //------ | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // This file is responsible for loading function blocks written in Python. | ||||||||||||||||||||||||||||||||||||||||||
| // Python function blocks communicate with the PLC runtime via shared memory. | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // Logging is done via function pointers that are set by the runtime after | ||||||||||||||||||||||||||||||||||||||||||
| // loading libplc.so. This avoids symbol resolution issues between the | ||||||||||||||||||||||||||||||||||||||||||
| // shared library and the main executable. | ||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||
| // Thiago Alves, Dec 2025 | ||||||||||||||||||||||||||||||||||||||||||
| //----------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| #include <errno.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <fcntl.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <pthread.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <stdio.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <stdlib.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <string.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <sys/mman.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <sys/stat.h> | ||||||||||||||||||||||||||||||||||||||||||
| #include <unistd.h> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| #include "include/iec_python.h" | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Function pointers for logging - set by python_loader_set_loggers() | ||||||||||||||||||||||||||||||||||||||||||
| // These are always initialized by symbols_init() before any Python FB code runs | ||||||||||||||||||||||||||||||||||||||||||
| static void (*py_log_info)(const char *fmt, ...); | ||||||||||||||||||||||||||||||||||||||||||
| static void (*py_log_error)(const char *fmt, ...); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Simple wrapper macros for logging | ||||||||||||||||||||||||||||||||||||||||||
| #define LOG_INFO(...) py_log_info(__VA_ARGS__) | ||||||||||||||||||||||||||||||||||||||||||
| #define LOG_ERROR(...) py_log_error(__VA_ARGS__) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| void python_loader_set_loggers(void (*log_info_func)(const char *, ...), | ||||||||||||||||||||||||||||||||||||||||||
| void (*log_error_func)(const char *, ...)) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| py_log_info = log_info_func; | ||||||||||||||||||||||||||||||||||||||||||
| py_log_error = log_error_func; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||
| * @brief Thread function that runs the Python script and logs its output | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * This function is spawned as a detached thread to run the Python interpreter | ||||||||||||||||||||||||||||||||||||||||||
| * and capture its stdout/stderr output for logging. | ||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||
| * @param arg The command string to execute (will be freed by this function) | ||||||||||||||||||||||||||||||||||||||||||
| * @return NULL | ||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||
| static void *runner_thread(void *arg) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| const char *cmd = (const char *)arg; | ||||||||||||||||||||||||||||||||||||||||||
| FILE *fp = popen(cmd, "r"); | ||||||||||||||||||||||||||||||||||||||||||
| if (fp == NULL) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| LOG_ERROR("[Python] Failed to start process: %s", cmd); | ||||||||||||||||||||||||||||||||||||||||||
| free((void *)cmd); | ||||||||||||||||||||||||||||||||||||||||||
| return NULL; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| char buffer[512]; | ||||||||||||||||||||||||||||||||||||||||||
| while (fgets(buffer, sizeof(buffer), fp) != NULL) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // Remove trailing newline if present | ||||||||||||||||||||||||||||||||||||||||||
| size_t len = strlen(buffer); | ||||||||||||||||||||||||||||||||||||||||||
| if (len > 0 && buffer[len - 1] == '\n') | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| buffer[len - 1] = '\0'; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| LOG_INFO("[Python] %s", buffer); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| pclose(fp); | ||||||||||||||||||||||||||||||||||||||||||
| free((void *)cmd); | ||||||||||||||||||||||||||||||||||||||||||
| return NULL; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| int create_shm_name(char *buf, size_t size) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| char shm_mask[] = "/tmp/shmXXXXXXXXXXXX"; | ||||||||||||||||||||||||||||||||||||||||||
| int fd = mkstemp(shm_mask); | ||||||||||||||||||||||||||||||||||||||||||
| if (fd == -1) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| LOG_ERROR("[Python loader] mkstemp failed: %s", strerror(errno)); | ||||||||||||||||||||||||||||||||||||||||||
| return -1; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| close(fd); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| snprintf(buf, size, "/%s", strrchr(shm_mask, '/') + 1); | ||||||||||||||||||||||||||||||||||||||||||
| unlink(shm_mask); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| int python_block_loader(const char *script_name, const char *script_content, char *shm_name, | ||||||||||||||||||||||||||||||||||||||||||
| size_t shm_in_size, size_t shm_out_size, void **shm_in_ptr, | ||||||||||||||||||||||||||||||||||||||||||
| void **shm_out_ptr, pid_t pid) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| char shm_in_name[256]; | ||||||||||||||||||||||||||||||||||||||||||
| char shm_out_name[256]; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Write the Python script to disk | ||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||
|
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); | |
| 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
AI
Dec 10, 2025
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.
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); |
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.
The second argument to
plugin_manager_get_funcappears to be a function pointer typevoid (*)(unsigned long), but this doesn't match the actual signature ofpython_loader_set_loggerswhich takes two variadic function pointers. This type mismatch could cause incorrect function resolution or undefined behavior.