11 — POLYMORPHISM

one loop, twelve machines — how the controller picks the right code at runtime.

Estimated 24 minutes.

Twelve machines and one question, every scan

A skid has twelve devices on it: four valves, three motors, two drives and three transmitters. Every scan the program has to ask all twelve the same thing — update yourself, and tell me if you are healthy. Written the usual way that is twelve call sites, and the thirteenth device means editing the program that calls them. Polymorphism is the answer to exactly that: one call site that works for every device, whatever it turns out to be.

Follow one call into three different bodies

The loop below is a single instruction. Step the index and watch where it actually lands — the array slot, the dispatch table it belongs to, and the block of code that finally runs.

What the loop is replacing

This is the trade the whole lesson is about: the ladder version is completely explicit and completely manual. The loop version is one line you never touch again.

Who decides which code runs?

Devices is an ARRAY[1..6] OF I_Device holding valves, motors and analog inputs. The loop calls Devices[i].Update(). At the moment that call executes, what determines which body runs?

  • The compiler, from the declared type of the array
  • The instance sitting in that slot, at the moment of the call
  • The loop index, through a hidden CASE the compiler generates
  • The order the instances were declared in

THIS^ — the address the body is standing on

One copy of FB_Valve.Update exists in memory, and it serves every valve in the plant. It manages that because every method call carries a hidden extra argument: the address of the instance it was called on. THIS is that pointer, and THIS^ is the instance itself. Most of the time you never write it — a bare Pos already means THIS^.Pos — but you need it the moment a local name shadows a member, and you need it whenever you want to hand your whole instance to somebody else.

The object form, in full

This is the real thing, as you would write it on CODESYS, TwinCAT or Machine Expert. TryPLC's browser engine implements the 2nd-edition core, so this source is shown rather than executed — the live stage two screens on runs the composition form of the same program.

How the runtime actually finds the code

Every function-block type that implements an interface gets one table of method addresses, built at compile time and shared by all its instances. Every instance of that type carries a pointer to its table in its own memory, ahead of its data. A call through an interface reference is three steps the tracer just drew: read the table pointer out of the instance, index it by the method's fixed slot number, and jump to whatever address is in that cell. Slot 0 is Update for every device type in the program — the numbering comes from the interface, not from the block.

Six machines, one loop, on the real engine

This is the composition form of the same program, executing scan by scan on the TryPLC engine: ARRAY[1..6] OF FB_Device, each element carrying its own Kind, one FOR loop calling all six. Every number on the stage is read back out of the interpreter.

The form that runs here

Two honest versions of one idea. The interface array pushes the choice into the runtime; the Kind input keeps the choice in your own source, where a code review can see it.

What does a virtual call cost?

You replace 64 direct FB calls with 64 calls through an interface array. What is the honest cost on a controller running a 5 ms task?

  • Nothing — the compiler resolves it back to direct calls
  • Roughly double the scan time; indirection is expensive
  • A few extra machine instructions per call, plus the loss of inlining and of static analysis
  • It depends on how many types implement the interface

Static binding, dynamic binding, and safety code

Static binding means the compiler wrote the destination into the instruction: Pump1.Update() can only ever reach one body. Dynamic binding means the destination is read from memory as the call happens. That difference is why functional-safety practice — the IEC 61508 family, and the MISRA-flavoured coding rules that follow it — restricts or forbids dynamic dispatch in the safety part of a system: you cannot demonstrate worst-case timing or full branch coverage for a call whose target is not known until it runs. The usual split on a real machine is polymorphism in the standard program and plain, statically bound blocks in the safety program.

Add a device type nobody planned for

Clip devices onto the panel rail. Both dispatchers are live: the hand-written IF-chain on the left, the interface loop on the right. Fit at least four types, and make sure one of them is a retrofit type that did not exist when Main was written.

Four places it genuinely pays

Alarm managers: 120 alarms, each with its own test, all evaluated by one loop that counts and publishes. Device managers: every field device asked to update and report health in one pass, so a new device type is a new block instead of a new branch. Recipe steps: a sequence held as an array of steps, each running its own logic, with the sequencer only advancing the index. HMI faceplate binding: one faceplate bound to whatever device the operator selected, because they all answer the same three calls.

And four places it does not

Polymorphism is a tool for lists of things, and it costs a level of indirection every time it is used. If there is no list, there is nothing to gain.

The retrofit meeting

A packaging line runs 40 devices through an interface array on TwinCAT. The customer is adding a servo-driven diverter — a device type that did not exist when the line was commissioned. What has to change in Main?

  • The loop, to add a branch for the new type
  • The interface, to add whatever the servo needs
  • Nothing in the loop — a new block implementing I_Device, and one new row in the device table
  • The array declaration, because a servo cannot satisfy a device interface

What you learned

  • Polymorphism is one call site reaching many different bodies — the call is fixed, the destination is not.
  • An ARRAY OF an interface type lets one FOR loop drive every device on a skid, whatever they are.
  • THIS is the hidden pointer every method call carries; THIS^ is the instance the body is running on.
  • Dispatch is three steps at runtime: read the instance's table pointer, index it by the method slot, jump.
  • Static binding is decided by the compiler; dynamic binding is decided by memory as the call happens.
  • The cost is a few instructions plus the loss of inlining and of static analysis — which is why safety code stays statically bound.
  • On a controller without interfaces, ARRAY[1..n] OF one FB type with a Kind input and a CASE does the same job, and it runs everywhere.

POLYMORPHISM — done

  • You can trace a call from a loop, through an interface reference and a method table, into the body that runs.
  • You can build a device manager that survives a new device type without editing the loop that drives it.
  • You can say, in a design review, exactly what dynamic dispatch costs and where it does not belong.