Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/src/plc_app/image_tables.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <stdlib.h>

#include "image_tables.h"
#include "include/iec_python.h"
#include "log.h"
#include "utils.h"

Expand Down Expand Up @@ -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");

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.
if (ext_python_loader_set_loggers)
{
ext_python_loader_set_loggers(log_info, log_error);
log_info("Python loader logging callbacks initialized");
}

return 0;
}

Expand Down
87 changes: 87 additions & 0 deletions core/src/plc_app/include/iec_python.h
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 */
215 changes: 215 additions & 0 deletions core/src/plc_app/python_loader.c
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

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.

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

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.
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;
}
23 changes: 14 additions & 9 deletions scripts/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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