Skip to main content

posts

TwinCAT: Implementing an API with a Hidden Global Reference

How to implement a TwinCAT API pattern where application code can call a shared service such as logging, alarms, or MQTT publishing without passing references through every project layer.

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_INTERFACE

The 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_INTERFACE

Keeping 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_VAR

The {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_METHOD

The 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_METHOD

The 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_IF

After 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.