The Problem
Infrastructure APIs often look simple from the outside.
A function block calls a logging API. An alarm is raised. An MQTT message is published. A message appears on the HMI.
Behind that small API call, something must do the actual work. There is usually one service instance that owns the connection to the outside world. It may talk to Windows, ADS, MQTT, an HMI, a database, or another task.
The awkward part is access to that service instance. Passing a reference through every layer of the PLC project works, but it pollutes the application structure. Suddenly every machine module knows about the logger, the alarm service, or the MQTT publisher. A fine way to turn infrastructure into everybody’s problem.
Beckhoff APIs often avoid this visible dependency. The call site does not show where the service instance lives. This article shows one way to implement a similar pattern in a TwinCAT library by using a hidden global reference.
This is still global state. The point is not to pretend otherwise. The point is to make the dependency small, controlled, and limited to infrastructure-style services.
Define the API Interface
Assume the library is called msglib. First define the interface that application code
uses to send a message.
INTERFACE IMessage
METHOD send
VAR_INPUT
message : STRING(255);
END_VAR
END_INTERFACEThe application should depend on this small API. It should not need to know whether the message is written to a log file, shown on an HMI, or sent with MQTT.
Define the Publisher Interface
The actual transport is handled by a publisher. It has a separate interface.
INTERFACE IPublisher
METHOD publish
VAR_INPUT
data : STRING(255);
END_VAR
END_INTERFACEKeeping IMessage and IPublisher separate avoids mixing the public API with the
implementation detail. The message API can stay stable even if the publisher changes.
Store the Hidden Reference
Create a global variable list named RefBase inside the library. The GVL contains the
publisher interface reference.
VAR_GLOBAL
{attribute 'hide'}
messenger : IPublisher;
END_VARThe {attribute 'hide'} pragma hides the variable from normal external access. That keeps
the application project from using RefBase.messenger directly.
There is a trade-off. The reference is also harder to inspect online.
Register the Publisher
Now implement a publisher and register it during an explicit initialization phase.
FUNCTION_BLOCK Publisher IMPLEMENTS IPublisher
VAR
initialized : BOOL;
END_VAR
METHOD init
IF NOT initialized THEN
RefBase.messenger := THIS^;
initialized := TRUE;
END_IF
END_METHOD
METHOD publish
VAR_INPUT
data : STRING(255);
END_VAR
// Send the data to the real transport here.
END_METHODThe application should create exactly one active publisher instance for this pattern. If multiple publishers register themselves, the last one wins. That may be intentional in a test setup, but it is usually a bug in a production machine.
Registration can also be done in FB_init, but that needs care. FB_init runs
automatically and is called again during online change. It also does not give the
application an obvious place to control initialization order. An explicit init() method is
usually easier to reason about and easier to test.
Implement the Message API
The API function block uses the hidden publisher reference.
FUNCTION_BLOCK Message IMPLEMENTS IMessage
METHOD send
VAR_INPUT
message : STRING(255);
END_VAR
IF RefBase.messenger <> 0 THEN
RefBase.messenger.publish(message);
END_IF
END_METHODThe validity check is important. An interface variable can be 0. If the message API is
called before the publisher is registered, the code must not blindly call through the
reference.
This example silently drops the message when no publisher is available. That keeps the code small, but it is not enough for most production systems. A real implementation should make the fault observable with a diagnostic flag, counter, event, or error state.
Use It from the Application
The application owns the concrete publisher instance and calls init() during startup.
PROGRAM MAIN
VAR
publisher : Publisher;
messageApi : Message;
initialized : BOOL;
END_VAR
IF NOT initialized THEN
publisher.init();
messageApi.send('Machine started');
initialized := TRUE;
END_IFAfter initialization, any function block that receives an IMessage reference, or owns a
Message instance, can send messages without knowing about the publisher instance.
Limitations
This pattern is useful, but it is not free.
Consider these limitations before using it:
- It is still global state, only hidden behind a small API.
- Initialization order matters. The publisher must be registered before messages are sent.
- Online-change behavior must be tested.
- Only one active publisher should normally register itself.
- Missing references need diagnostics. Silently dropping messages can hide real faults.
- Hidden references are harder to debug than explicit dependencies.
Use this pattern for infrastructure services that are naturally shared across the project: logging, alarms, events, diagnostics, or message publishing. Do not use it to share random machine state between unrelated function blocks. That is not architecture. That is a future incident report with syntax highlighting.
Conclusion
A hidden global reference can make a TwinCAT API easier to use when many parts of the application need access to one shared service, such as logging, alarms, events, or MQTT publishing.
The benefit is a cleaner application interface. Function blocks can call a small API without carrying a publisher reference through every layer of the project.
The cost is hidden coupling. The publisher must be registered before the API is used,
online-change behavior must be tested, and missing references must be handled explicitly.
The {attribute 'hide'} pragma hides the variable from normal use, but it does not remove
the dependency.
Use this pattern for infrastructure-style services with one well-defined implementation instance. Do not use it as a general escape hatch for sharing arbitrary state across the PLC project.
