Overview
Siemens WinCC V7 (and the V7.x service packs) supports calling external dynamic-link libraries (DLLs) directly from C / VBS actions attached to buttons, pictures, or scheduled tasks. This pattern is the standard way to integrate a custom Visual C++ module that talks to third-party hardware (RFID readers, barcode scanners, weigh scales, custom serial devices) without rebuilding the WinCC runtime in C. The integration path is short, but it is strict: the DLL must export C-linkage symbols with __declspec(dllexport), the file must be visible to the WinCC runtime (BIN folder or PATH), and the action script must wrap every native call in #pragma code("dllgraph.dll") / #pragma code() so the WinCC C interpreter loads the binary stub correctly.
This tutorial consolidates the standard Siemens procedure with a working C++ project, the recommended Build / Deploy steps, and a troubleshooting matrix for the most common failure modes (function not found, error return, silent no-op, GetComputerNameEx returning blanks).
#pragma code() pattern, or VB via .NET assemblies) - confirm your target platform before copying samples.Prerequisites
- WinCC V7.x installed and licensed, with the "C-Script" option active in the project properties (right-click the project in WinCC Explorer → Properties → Options tab).
- Microsoft Visual C++ build environment. Visual Studio 2010 through 2019 are confirmed compatible with WinCC V7 C-scripts. Target the same architecture (x86 or x64) as the WinCC runtime - mixed-bit DLLs are the most common cause of silent load failures.
-
Administrator rights on the engineering station to write into
\\\\<project>\\bin\\and to update thePATHenvironment variable. - Read access to the WinCC Information System (installed locally or via Siemens Industry Online Support) for the chapter "ANSI-C function descriptions → Standard functions → code".
Architecture: How WinCC Loads a DLL
WinCC C actions are compiled at runtime by the dllgraph.dll interpreter. When a script reaches a foreign function call, WinCC must resolve the symbol at execution time using the standard Windows loader rules:
- The
#pragma code("YourDll.dll")directive tells the interpreter to bind subsequent function calls toYourDll.dll. - Windows searches the following locations, in order, for the DLL: the directory of the running process (
explorer.exefor WinCC runtime), the current working directory, the Windows system directory, the Windows directory, and finally each folder listed in thePATHenvironment variable. - If the loader finds the file, it maps it into the WinCC process and binds exported symbols. If the file is missing, the action returns the string
errorat runtime - the same symptom a student would see when calling a function that does not exist.
A DLL is, by definition, a shared module that can be loaded into any process that has the right import table. For background on the Windows loader behavior see the Microsoft Learn article Dynamic link library (DLL) and the general reference at Dynamic-link library (Wikipedia).
Step 1 - Build a C++ DLL with C-Linkage Exports
Create a new Win32 Dynamic-Link Library project in Visual Studio. The single most important rule is that the functions WinCC will call must be exported with C linkage and the __declspec(dllexport) storage class. Without the C linkage, the C++ compiler mangles the symbol name (?init_port3@@YAXXZ) and the WinCC interpreter - which looks up the undecorated name - cannot find it.
Header file: RfidPort.h
#ifdef RFIDPORT_EXPORTS
#define RFIDPORT_API extern "C" __declspec(dllexport)
#else
#define RFIDPORT_API extern "C" __declspec(dllimport)
#endif
RFIDPORT_API int init_port3(void);
RFIDPORT_API int read_uid(char* buf, int buflen);
RFIDPORT_API void close_port3(void);
Source file: RfidPort.cpp
#include "RfidPort.h"
#include <windows.h>
RFIDPORT_API int init_port3(void)
{
HANDLE h = CreateFileA("\\\\.\\COM3",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return -1;
return 0;
}
RFIDPORT_API int read_uid(char* buf, int buflen)
{
// ... real read implementation ...
return 0;
}
RFIDPORT_API void close_port3(void)
{
// ... cleanup ...
}
Project settings to verify
| Setting | Value | Notes |
|---|---|---|
| Configuration Type | Dynamic Library (.dll) | NOT .exe |
| Character Set | Use Multi-Byte / Not Set | Match WinCC V7 build (ANSI C) |
| Platform | x86 if WinCC is x86, x64 if WinCC is x64 | Mixed-bit is the #1 cause of silent failure |
| Preprocessor definition | RFIDPORT_EXPORTS |
Switches the macro to dllexport side |
| Runtime Library | Multi-threaded DLL (/MD) | Matches MSVCRT shipped with WinCC |
Build the project. The output RfidPort.dll is the file you will deploy to WinCC.
Step 2 - Deploy the DLL to a Location WinCC Can Find
WinCC runtime runs under the account that started the WinCC Runtime. Windows DLL search order will only look in the directories listed above. The two recommended deployment locations are:
-
Project BIN folder:
C:\Siemens\WinCC\WinCCProjects\<YourProject>\bin\. DropRfidPort.dllhere. This is the path WinCC uses for its owndllgraph.dll, so the loader will always find co-located files. -
System PATH: Add the folder containing
RfidPort.dllto thePATHenvironment variable. Restart the WinCC Runtime after the change. Use this for shared libraries that several projects use.
PATH only affect processes started after the change. Close WinCC Runtime (and any background CCWrite.exe, CCAlgHl7.exe processes) and reopen the project. The Windows loader also has a per-process snapshot of the path; the SetDllDirectory API is not relevant here because WinCC controls its own loader calls.If the runtime is on a different machine from the engineering station, copy the DLL to the same project folder on the runtime PC and confirm the bitness. A 32-bit RfidPort.dll on a 64-bit runtime (or vice-versa) will not load and will return a runtime error string in the action output log.
Step 3 - Call the DLL from a WinCC C Action
Open Graphics Designer, drop a button on a screen, and configure a C action on the mouse-click event:
- Right-click the button → Properties → Events → "Mouse" → "Press left".
- Assign a C action (not a VBS action - VBS uses
CreateObject("ComObject")for COM servers, not raw DLLs). - Paste the code below:
#include "apdefap.h"
void OnClick(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName, UINT nFlags, int nX, int nY)
{
DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1;
char szName[MAX_COMPUTERNAME_LENGTH + 1];
#pragma code("kernel32.dll")
BOOL WINAPI GetComputerNameExA(DWORD, LPSTR, LPDWORD);
#pragma code()
if (GetComputerNameExA(3, szName, &dwSize))
printf("Computer name: %s\r\n", szName);
else
printf("GetComputerNameEx failed: %lu\r\n", GetLastError());
}
For the custom RFID DLL, the script is:
#pragma code("RfidPort.dll")
int init_port3(void);
int read_uid(char* buf, int buflen);
void close_port3(void);
#pragma code()
void OnClick(char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName, UINT nFlags, int nX, int nY)
{
int rc = init_port3();
if (rc != 0) {
printf("init_port3 failed, rc=%d\r\n", rc);
return;
}
char uid[64] = {0};
read_uid(uid, sizeof(uid));
printf("UID = %s\r\n", uid);
close_port3();
}
Notice the four-line block at the top: the prototypes are declared between #pragma code("RfidPort.dll") and #pragma code(). This pair of pragmas is mandatory; the WinCC compiler inserts the __declspec(dllimport) declaration only for symbols listed inside the block. Outside the block, calls to those names will fail to link.
Step 4 - Build the WinCC Project and Activate Runtime
- In WinCC Explorer, right-click the project → "Rebuild". This compiles the C actions and refreshes the
bin\directory of the runtime image. - Open Graphics Designer, right-click the picture → "Assign picture to ..." (if not already assigned).
- Open WinCC Runtime (Start → Programs → Siemens Automation → WinCC → WinCC Runtime). Click the button. The output of
printfis written to the WinCC diagnostic log (<project>\Diagnostics\WinCC_Sys_*.log) and to theAPDIAGoutput window of the GDI runtime.
Verification
| Check | Method | Expected Result |
|---|---|---|
| DLL visible to loader | Sysinternals Process Explorer → CCRtSvc.exe → DLL list |
RfidPort.dll appears after first call |
| Symbol export |
dumpbin /exports RfidPort.dll from a VS command prompt |
init_port3, read_uid, close_port3 listed as exported (undecorated C names) |
| Bit-ness |
dumpbin /headers RfidPort.dll → machine type |
x86 for 32-bit WinCC, x64 for 64-bit WinCC |
| Runtime log | Tail WinCC_Sys_*.log in the project Diagnostics folder |
Line with the printf() output of the action |
| Function entry | Add OutputDebugStringA("init_port3 entered"); at the top of the function and run DebugView |
DebugView shows the string the first time the button is pressed |
Troubleshooting Matrix
| Symptom | Likely Root Cause | Fix |
|---|---|---|
Action output is the literal string error
|
DLL not found in any search path, or symbol not exported with C linkage | Drop DLL into bin\ folder; verify __declspec(dllexport) with extern "C"; re-run dumpbin /exports
|
| No log entry at all, no error | Pragmas missing or action not assigned correctly | Wrap every native prototype in #pragma code("YourDll.dll") ... #pragma code(); confirm action is C, not VBS |
Returns blank string for GetComputerNameExA
|
First parameter (name type) wrong, or buffer size uninitialized | Use 3 for ComputerNamePhysicalDnsHostname; initialize dwSize = MAX_COMPUTERNAME_LENGTH + 1
|
| Works on engineering station, fails on runtime | DLL not deployed to the runtime PC, or different bitness | Copy DLL to runtime bin\; match x86/x64 with WinCC runtime build |
Loader returns 0xC000007B (STATUS_INVALID_IMAGE_FORMAT) |
32/64-bit mismatch | Rebuild the DLL for the same platform as the WinCC runtime |
Loader returns 0xC0000135 (STATUS_DLL_NOT_FOUND) |
Dependent runtime DLL (e.g. VCRUNTIME140) missing | Install the matching Visual C++ Redistributable on the runtime PC, or rebuild with /MT static linking |
| Action compiles, first call crashes WinCC Runtime | Calling convention mismatch (__cdecl vs __stdcall) or stack corruption from a bad pointer |
Use WINAPI for Win32 API wrappers; validate all buffer pointers WinCC passes in |
Common Pitfalls
Mixing C and C++ linkage. If the prototype inside the #pragma code() block is declared as extern "C++" int init_port3(); the symbol will be C++-mangled and unresolvable. Keep the prototypes plain C.
Calling convention. Win32 APIs use WINAPI (= __stdcall). User C functions are typically __cdecl (the Visual C++ default). Mismatched conventions corrupt the stack and crash the runtime the first time the function returns. Use WINAPI on the prototype inside the pragma block when wrapping a Win32 API:
#pragma code("kernel32.dll")
BOOL WINAPI GetComputerNameExA(DWORD, LPSTR, LPDWORD);
#pragma code()
Static vs. dynamic CRT. The default Visual Studio CRT is the DLL variant (/MD). The WinCC runtime already ships with the matching MSVCRxxx.dll. If you switch to /MT (static), the binary grows but you remove one possible STATUS_DLL_NOT_FOUND cause on air-gapped runtime PCs.
Runtime version drift. A DLL built on Visual Studio 2019 against the v142 toolset will require vcruntime140.dll and msvcp140.dll on the target. Match the Visual Studio toolset to the redistributable installed on every WinCC Runtime PC, or use /MT as a workaround.
Data execution across action boundaries. The runtime may unload the script module between actions, so any state your DLL keeps in process-local globals survives but the DLL file itself can be reloaded. Do not store connection handles in the WinCC tag system; store them inside the DLL and key them by an integer handle returned to the C action.
Reference: WinCC C Pragma and Standard Functions
WinCC defines code as a documented standard function in the ANSI-C for creating functions and actions reference. The exact syntax is:
#pragma code("dllname.dll")
<function prototypes>
#pragma code()
The official Siemens FAQ at Entry ID 8301801 - How do you call a Windows DLL function in a WinCC C script? covers the kernel32 sample used in this tutorial, the GetComputerNameEx implementation, and the recommended WINAPI calling convention. The general WinCC scripting documentation is searchable from Siemens Industry Online Support under "WinCC V7.x > Function descriptions > ANSI-C function descriptions > Standard functions > code".
Field-Commissioning Checklist
- Confirm the WinCC project is rebuilt after dropping the DLL into
bin\. - Confirm
dumpbin /exportsshows the undecorated C symbols. - Confirm x86 / x64 parity between the DLL and the WinCC runtime.
- Add a temporary
OutputDebugStringAin the DLL entry point to verify it is loaded on first use. - Tail the WinCC diagnostic log (
WinCC_Sys_*.log) for theprintf()outputs of the C action. - Remove the temporary debug output before sign-off.
Notes for Multi-Project / Multi-Runtime Deployments
When the same DLL is shared between several WinCC projects on the same runtime PC, prefer the PATH approach with a versioned subfolder (e.g. C:\Siemens\SharedLibs\RfidPort\1.4\) and document the version inside the project README. Avoid copying the DLL into each project's bin\ folder; that path makes upgrades and roll-backs painful. When upgrading the DLL, stop the WinCC Runtime, swap the file, then start the runtime - the Windows loader keeps the old copy mapped until the process exits.
Why does my WinCC C action return the literal string "error" when I call a DLL function?
The runtime could not bind the function symbol. In 90% of cases the DLL is not on the loader search path (drop it into the project bin\ folder or add its folder to PATH) or the symbol is exported with C++ linkage and is therefore name-mangled. Re-declare the prototype as extern "C" __declspec(dllexport) and confirm with dumpbin /exports YourDll.dll that the undecorated name is present.
Do I have to wrap every native call in #pragma code("name.dll") ... #pragma code()?
Yes. The pair of pragmas tells the WinCC C compiler to emit __declspec(dllimport) for the prototypes listed between them. Without the pragmas the linker has no import library for the symbol and the action silently fails at runtime. Put the prototypes only for the DLL you are about to call, then close the block with #pragma code() before any standard C code.
My DLL works on the engineering station but not on the runtime PC - what changed?
Two usual suspects: the DLL was not copied to the runtime project, or the bitness does not match. Confirm dumpbin /headers RfidPort.dll shows x86 on a 32-bit WinCC Runtime and x64 on a 64-bit WinCC Runtime. A 32/64 mismatch raises 0xC000007B at load time and the action falls back to the generic error string in the diagnostic log.
Can I use a C++ class exported from a DLL, or only plain C functions?
WinCC C actions only resolve undecorated C symbols, so the cleanest path is to wrap your C++ class with a plain C interface (a factory function returning an opaque handle, plus a small set of C functions that call the class methods internally). Returning C++ objects directly into the WinCC script is not supported because the two compilers do not agree on ABI, name mangling, or exception unwinding.
Where can I find the official Siemens example for calling kernel32.dll from a WinCC C action?
Siemens publishes the canonical GetComputerNameEx example as FAQ entry 8301801, "How do you call a Windows DLL function in a WinCC C script?", reachable from the Siemens Industry Online Support portal at support.automation.siemens.com/WW/view/en/8301801.