diff --git a/core/src/plc_app/image_tables.c b/core/src/plc_app/image_tables.c index 14a41bcc..d67834df 100644 --- a/core/src/plc_app/image_tables.c +++ b/core/src/plc_app/image_tables.c @@ -2,6 +2,7 @@ #include #include "image_tables.h" +#include "include/iec_python.h" #include "log.h" #include "utils.h" @@ -102,6 +103,16 @@ int symbols_init(PluginManager *pm) dint_input, dint_output, lint_input, lint_output, int_memory, dint_memory, lint_memory); + // 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"); + if (ext_python_loader_set_loggers) + { + ext_python_loader_set_loggers(log_info, log_error); + log_info("Python loader logging callbacks initialized"); + } + return 0; } diff --git a/core/src/plc_app/include/iec_python.h b/core/src/plc_app/include/iec_python.h new file mode 100644 index 00000000..8afca3bb --- /dev/null +++ b/core/src/plc_app/include/iec_python.h @@ -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 . +//------ +// +// 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 +#include + +#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 */ diff --git a/core/src/plc_app/python_loader.c b/core/src/plc_app/python_loader.c new file mode 100644 index 00000000..1883a7b5 --- /dev/null +++ b/core/src/plc_app/python_loader.c @@ -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 . +//------ +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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); + + LOG_INFO("[Python loader] Random shared memory location: %s", shm_name); + + snprintf(shm_in_name, sizeof(shm_in_name), "%s_in", shm_name); + snprintf(shm_out_name, sizeof(shm_out_name), "%s_out", shm_name); + + // Write script content with format specifiers replaced + fprintf(fp, script_content, pid, shm_name, shm_name); + fflush(fp); + fsync(fileno(fp)); + fclose(fp); + + // Map shared memory for inputs + int shm_in_fd = shm_open(shm_in_name, O_CREAT | O_RDWR, 0660); + if (shm_in_fd < 0) + { + LOG_ERROR("[Python loader] shm_open (input) error: %s", strerror(errno)); + return -1; + } + if (ftruncate(shm_in_fd, shm_in_size) == -1) + { + LOG_ERROR("[Python loader] ftruncate (input) error: %s", strerror(errno)); + close(shm_in_fd); + return -1; + } + *shm_in_ptr = mmap(NULL, shm_in_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_in_fd, 0); + if (*shm_in_ptr == MAP_FAILED) + { + LOG_ERROR("[Python loader] mmap (input) error: %s", strerror(errno)); + close(shm_in_fd); + return -1; + } + + // Map shared memory for outputs + int shm_out_fd = shm_open(shm_out_name, O_CREAT | O_RDWR, 0660); + if (shm_out_fd < 0) + { + LOG_ERROR("[Python loader] shm_open (output) error: %s", strerror(errno)); + close(shm_in_fd); + munmap(*shm_in_ptr, shm_in_size); + shm_unlink(shm_in_name); + return -1; + } + if (ftruncate(shm_out_fd, shm_out_size) == -1) + { + LOG_ERROR("[Python loader] ftruncate (output) error: %s", strerror(errno)); + close(shm_out_fd); + close(shm_in_fd); + munmap(*shm_in_ptr, shm_in_size); + shm_unlink(shm_in_name); + return -1; + } + *shm_out_ptr = mmap(NULL, shm_out_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_out_fd, 0); + if (*shm_out_ptr == MAP_FAILED) + { + LOG_ERROR("[Python loader] mmap (output) error: %s", strerror(errno)); + close(shm_out_fd); + close(shm_in_fd); + munmap(*shm_in_ptr, shm_in_size); + shm_unlink(shm_in_name); + return -1; + } + + // Close file descriptors (mapping remains valid) + close(shm_in_fd); + close(shm_out_fd); + + // Prepare command to run Python script + char *cmd = malloc(512); + if (cmd == NULL) + { + LOG_ERROR("[Python loader] malloc failed for cmd buffer"); + return -1; + } + snprintf(cmd, 512, "python3 -u %s 2>&1", script_name); + + // Spawn thread to run Python process + pthread_t tid; + if (pthread_create(&tid, NULL, runner_thread, cmd) != 0) + { + LOG_ERROR("[Python loader] pthread_create failed: %s", strerror(errno)); + free(cmd); + return -1; + } + pthread_detach(tid); + + LOG_INFO("[Python loader] Started Python function block: %s", script_name); + + return 0; +} diff --git a/scripts/compile.sh b/scripts/compile.sh index 60317d42..ef732e24 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -6,12 +6,14 @@ ROOT="core/generated" LIB_PATH="$ROOT/lib" SRC_PATH="$ROOT" BUILD_PATH="build" +PYTHON_INCLUDE_PATH="core/src/plc_app/include" +PYTHON_LOADER_SRC="core/src/plc_app/python_loader.c" FLAGS="-w -O3 -fPIC" check_required_files() { local missing_files=() - + if [ ! -f "$SRC_PATH/Config0.c" ]; then missing_files+=("$SRC_PATH/Config0.c") fi @@ -27,7 +29,7 @@ check_required_files() { if [ ! -d "$LIB_PATH" ]; then missing_files+=("$LIB_PATH (directory)") fi - + if [ ${#missing_files[@]} -ne 0 ]; then echo "[ERROR] Missing required source files:" >&2 printf ' %s\n' "${missing_files[@]}" >&2 @@ -46,17 +48,20 @@ fi # Compile objects into build/ echo "[INFO] Compiling Config0.c..." -gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/Config0.c" -o "$BUILD_PATH/Config0.o" +gcc $FLAGS -I "$LIB_PATH" -I "$PYTHON_INCLUDE_PATH" -include iec_python.h -c "$SRC_PATH/Config0.c" -o "$BUILD_PATH/Config0.o" echo "[INFO] Compiling Res0.c..." -gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/Res0.c" -o "$BUILD_PATH/Res0.o" +gcc $FLAGS -I "$LIB_PATH" -I "$PYTHON_INCLUDE_PATH" -include iec_python.h -c "$SRC_PATH/Res0.c" -o "$BUILD_PATH/Res0.o" echo "[INFO] Compiling debug.c..." -gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/debug.c" -o "$BUILD_PATH/debug.o" +gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/debug.c" -o "$BUILD_PATH/debug.o" echo "[INFO] Compiling glueVars.c..." -gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/glueVars.c" -o "$BUILD_PATH/glueVars.o" +gcc $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/glueVars.c" -o "$BUILD_PATH/glueVars.o" echo "[INFO] Compiling c_blocks_code.cpp..." -g++ $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/c_blocks_code.cpp" -o "$BUILD_PATH/c_blocks_code.o" +g++ $FLAGS -I "$LIB_PATH" -c "$SRC_PATH/c_blocks_code.cpp" -o "$BUILD_PATH/c_blocks_code.o" +echo "[INFO] Compiling python_loader.c..." +gcc $FLAGS -I "core/src/plc_app" -c "$PYTHON_LOADER_SRC" -o "$BUILD_PATH/python_loader.o" # Link shared library into build/ -echo "[INFO] Compiling shared library..." +echo "[INFO] Linking shared library..." g++ $FLAGS -shared -o "$BUILD_PATH/new_libplc.so" "$BUILD_PATH/Config0.o" \ - "$BUILD_PATH/Res0.o" "$BUILD_PATH/debug.o" "$BUILD_PATH/glueVars.o" "$BUILD_PATH/c_blocks_code.o" + "$BUILD_PATH/Res0.o" "$BUILD_PATH/debug.o" "$BUILD_PATH/glueVars.o" \ + "$BUILD_PATH/c_blocks_code.o" "$BUILD_PATH/python_loader.o" -lpthread -lrt