Resolving OPC UA C++ SDK Linux Linker Errors in Custom Builds

Erik Lindqvist11 min read
OPC / OPC UAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The custom Linux OPC UA C++ server target failed at link time because its hand-maintained build did not match the example’s complete source list, SDK feature definitions, include roots, and library order. The deciding observation is whether compilation stops on a missing header or the final link stops on undefined reference errors. Use the SDK’s matching CMake example as the baseline, then reproduce its configuration in a custom Makefile rather than correcting one flag at a time.

Build-stage signatures in the Linux OPC UA example

The first build compiled servermain.cpp, shutdown.cpp, and opcserver.cpp, then failed on the command that linked those objects with SDK libraries. That boundary matters: the compiler accepted the translation units, while the linker could not resolve implementations referenced by objects selected from the SDK archives. The two uaunistring.h type-qualifier warnings were not fatal and do not explain unresolved symbols.

A later build stopped earlier, while compiling a different hello-world target, with uaserverapplication.h: No such file or directory. That is a header-search or SDK-layout problem, not the same failure as a missing function implementation. Applying a different library order cannot repair a missing include file.

Build observation Failure boundary What to inspect
Objects compile, then undefined reference appears after g++ -o Link stage Object list, library availability, feature configuration, and left-to-right library order
Fatal error: ... No such file or directory during a g++ -c command Compile stage Whether the header exists in the selected SDK tree and whether the command includes its parent directory
Warnings appear, but compilation continues Usually nonfatal compiler diagnostic Check whether the command later produces an object; diagnose the eventual fatal error separately
The generated build reports that configuration is unchanged and skips qmake Build-system generation Whether the active Makefile reflects the edited source list, macros, and include paths

Read the command immediately preceding the first fatal diagnostic. For a compile error, that command is normally a g++ -c invocation with -I paths. For a link error, inspect the complete g++ -o invocation, including every object and every -l option. The stage determines which parts of the build definition can affect the result.

Archive resolution behind the undefined references

The first failure names objects inside libuamoduled.a and then lists unresolved symbols such as UaSemaphore::UaSemaphore, UaSemaphore::post, UaPkiCertificate::fromDER, and several user-identity-token constructors. This shows that the link included the server archive and selected its server implementation object, but the final link did not resolve all of that object’s dependencies.

Static libraries are archives of object files. In a conventional GNU link, the linker processes inputs from left to right and extracts an archive member when it is needed to satisfy a reference already seen. Therefore, an archive that provides symbols for an earlier object generally belongs later on the link line. Listing all expected library names is not enough: the archives must be present, compatible with the selected SDK configuration, and ordered so that the linker can extract the required members.

The unresolved names are not a single missing application function. They span synchronization, PKI certificate handling, and identity-token functionality referenced by the server implementation. That breadth points toward an incomplete or incorrectly composed SDK link configuration rather than a defect in one line of the hello-world application. A preprocessor definition affects which declarations and code are compiled; it does not itself supply a missing implementation from a library.

Do not infer that every unresolved name needs another arbitrary -l option. Compare the full command against the build configuration for the same SDK package and target. An archive can be named on the command line yet fail to satisfy a symbol when the archive variant or feature configuration does not match, or when its position causes the needed member not to be selected.

Dependency order in the server link command

The working hand-written Makefile in this case places the application objects before the SDK and system libraries, then orders SDK archives as follows. This is a reported working order for the project layout and SDK build represented here; use the CMake-generated link line for the installed package if its library set differs.

LIBS = -L../../lib -luamodule -luamodels -lcoremodule -lxmlparser -luabase -luastack -lxml2 -lpthread -lrt -lssl -lcrypto -luapki

The final link rule needs to put the objects before $(LIBS), as in the successful recipe:

$(TARGET): $(OBJECTS)
	$(CXX) -o $@ $(OBJECTS) $(LIBS)

One failed retry changed to unsuffixed library names but used a shorter, differently ordered list. It still omitted serverconfigxml.cpp from the objects and did not reproduce the working link command. Changing a library suffix or adding a few system libraries alone does not recreate the target’s dependency graph. Keep one coherent SDK library set; compare it with the package’s generated build rather than mixing the earlier d-suffixed names with the later unsuffixed names.

The server-only target also does not need the client include directory or client library unless the application itself uses client APIs. Removing unused client dependencies makes the target definition clearer; it is not a substitute for the correct server libraries.

Feature macros and include roots for the SDK

The working server configuration includes more feature definitions than the first project supplied. Keep the names and values aligned with the SDK example and the libraries being linked:

-DSUPPORT_XML_CONFIG
-D_UA_STACK_USE_DLL
-DSUPPORT_Method_Server_Facet=1
-DSUPPORT_Event_Subscription_Server_Facet=1
-DSUPPORT_Historical_Access=1
-DSUPPORT_Node_Management_Server_Facet=1
-DOPCUA_SUPPORT_SECURITYPOLICY_BASIC128RSA15=1
-DOPCUA_SUPPORT_SECURITYPOLICY_BASIC256=1
-DOPCUA_SUPPORT_SECURITYPOLICY_NONE=1
-DOPCUA_SUPPORT_PKI=1

The first attempt already defined the security-policy and PKI options, but not the XML configuration and server-facet options listed above. A later attempt added those definitions but continued to fail at link time. That sequence is a useful diagnostic: the definitions are part of the target configuration, but adding them does not fix an omitted source file or incorrect archive order by itself.

The reported working Makefile used these include roots, relative to its project directory:

-I../../include/uapki
-I../../include/uabase
-I../../include/uastack
-I../../include/uaserver
-I../../include/xmlparser
-I../simulation_buildingautomation
-I.
-I./linux

Use the include roots that exist in the checked-out SDK layout and verify them against the generated compile command. An include option must identify a directory from which the compiler can resolve the header’s path as written in the source. Similar-looking relative paths can point somewhere else when the Makefile runs from a different working directory.

Complete source set for the hand-built server

The working target includes four C++ source files and their corresponding objects:

SOURCES = main.cpp opcserver.cpp serverconfigxml.cpp shutdown.cpp
OBJECTS = main.o opcserver.o serverconfigxml.o shutdown.o

That list addresses a defect in the retry Makefile, which showed main.cpp and opcserver.c and linked only main.o and opcserver.o. The working example uses opcserver.cpp, not opcserver.c, and includes serverconfigxml.cpp and shutdown.cpp. In particular, the second set of unresolved symbols named ServerConfigXml methods and type information from the opcserver.o vtable. The successful source list compiles the corresponding implementation file into the target.

For a custom project, take the source list from the matching example and remove a file only after checking that no remaining object references its definitions. A header being present in an include directory does not replace compiling the source file that implements its methods. Conversely, adding a source file will not correct an include error if the compiler cannot find one of its headers.

A compact form of the reported working Makefile is:

CXX = g++
DEFINES = -DSUPPORT_XML_CONFIG -D_UA_STACK_USE_DLL -DSUPPORT_Method_Server_Facet=1 -DSUPPORT_Event_Subscription_Server_Facet=1 -DSUPPORT_Historical_Access=1 -DSUPPORT_Node_Management_Server_Facet=1 -DOPCUA_SUPPORT_SECURITYPOLICY_BASIC128RSA15=1 -DOPCUA_SUPPORT_SECURITYPOLICY_BASIC256=1 -DOPCUA_SUPPORT_SECURITYPOLICY_NONE=1 -DOPCUA_SUPPORT_PKI=1
CXXFLAGS = -pipe -Wall -W $(DEFINES)
INCPATH = -I../../include/uapki -I../../include/uabase -I../../include/uastack -I../../include/uaserver -I../../include/xmlparser -I../simulation_buildingautomation -I. -I./linux
LIBS = -L../../lib -luamodule -luamodels -lcoremodule -lxmlparser -luabase -luastack -lxml2 -lpthread -lrt -lssl -lcrypto -luapki
SOURCES = main.cpp opcserver.cpp serverconfigxml.cpp shutdown.cpp
OBJECTS = main.o opcserver.o serverconfigxml.o shutdown.o
TARGET = hello_world

all: $(TARGET)

$(TARGET): $(OBJECTS)
	$(CXX) -o $@ $(OBJECTS) $(LIBS)

.cpp.o:
	$(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<

Retain the relative include and library paths only when the project runs from the same directory depth and SDK layout. This recipe records the working composition; it is not a replacement for checking the paths and library variants in the installed SDK.

CMake baseline for the same SDK package

The project that prompted the first report built the supplied examples when using the provided CMake script. The SDK support response also pointed to the delivered application CMakeLists.txt files and noted that CMake can generate project files for other development environments. Use that example build as the comparison point before maintaining a separate Makefile.

  1. Build the supplied server example with the CMake configuration belonging to the installed SDK package.
  2. Record its compile definitions, include paths, source files, library names, and final link order.
  3. Compare the custom target against those values, then reproduce the target-specific configuration instead of copying only the library list.
  4. Regenerate the custom build files after changing project inputs. The first log reported that configuration was unchanged and qmake skipped its generation step, so a clean alone did not demonstrate that the active build definition had been regenerated.

A later, separate CMake attempt failed while compiling servermain.cpp because the compiler could not find uaserverapplication.h. CMake does not resolve an absent header automatically: inspect whether that header exists in the installed SDK tree and whether the generated compile command points to the directory containing it. If the file is absent from the selected tree, check that the project and SDK installation are from the intended package before changing linker settings.

Verification from generated commands and executable output

Verify one build stage at a time. This keeps a compile-path defect from being hidden under linker edits and confirms that the final target uses the same source and library configuration that was tested independently.

  1. Check the source and object list. Confirm that each expected translation unit is compiled and that the final link command includes its object. For the reported target, check main.o, opcserver.o, serverconfigxml.o, and shutdown.o.
  2. Check each compile command. Compare the feature definitions and -I roots with the working target. If uaserverapplication.h is missing, verify the file and the relevant include root before proceeding to the link stage.
  3. Check the final link command as a whole. Confirm -L../../lib, the selected SDK library set, system libraries, and left-to-right order. A list that merely contains familiar names is not enough.
  4. Check the result of the link. The target should be produced without unresolved references. If it is not, use the first unresolved symbol and the archive or object named beside it to trace which part of the dependency composition remains incomplete.
  5. Run the resulting server example. A successful link verifies symbol resolution, not server runtime behavior; launch the executable using the example’s normal configuration and check that it starts as expected for that project.

Keep the generated commands with the test results. They expose the actual configuration used by the compiler and linker, whereas a Makefile fragment or IDE setting can describe intended values that the active generated build never consumed.

Recurring configuration traps in Linux SDK builds

Several different corrections were needed in the hand-built target, which is why repeating the same link attempt after changing one option did not converge:

  • Using only the initial library list. The original list included the client library and omitted XML parser and system dependencies present in the working recipe. A server target should include the dependencies required by its selected server features, not a copied partial list.
  • Changing library names without matching the package. The first command used names ending in d; a retry used names without that suffix. The successful recipe used the latter set. The suffix alone does not identify a compatible configuration; compare the selected files and generated link line for the package in use.
  • Correcting macros but leaving sources or order incomplete. The retry added the recommended feature definitions yet still reported undefined references, including ServerConfigXml methods. The final recipe also corrected the source list and archive order.
  • Using a stale generated Makefile. The initial output explicitly skipped qmake because the configuration was unchanged. When the project file changes, verify that the regenerated commands reflect the change rather than treating a successful clean as proof.
  • Confusing missing headers with missing implementations. uaserverapplication.h is resolved during compilation; UaSemaphore and UaPkiCertificate references are resolved during linking. Diagnose each from the failing command, not from the broad label “compile failed.”

For a missing header, compare the exact spelling in the include directive with the SDK tree and the compiler’s search roots. For unresolved symbols, compare the target’s implementation objects, macro set, SDK archive variants, and archive order. These checks isolate the repair without adding unrelated dependencies.

FAQ

How do I fix undefined references in an OPC UA C++ server build?

Match the SDK example’s source files, feature macros, include roots, and library set, then put the object files before the libraries. The reported working order was -luamodule -luamodels -lcoremodule -lxmlparser -luabase -luastack -lxml2 -lpthread -lrt -lssl -lcrypto -luapki after -L../../lib.

How do I know whether the problem is a header path or link order?

A No such file or directory diagnostic during g++ -c means the compiler cannot resolve a header through its include roots. An undefined reference after g++ -o means the link inputs or their order did not resolve a symbol.

How do I choose the source files for the hello-world target?

The reported working target compiled main.cpp, opcserver.cpp, serverconfigxml.cpp, and shutdown.cpp. In particular, use the C++ source opcserver.cpp shown by the working recipe, not the retry’s opcserver.c.

When should I stop changing libraries and escalate?

Stop when the exact SDK example configuration is in use but the same first error remains, or when a required header is missing from the selected SDK tree. Send Unified Automation support the SDK package/build variant, generated compile and link commands, complete first diagnostic, and source list so they can identify a package or configuration mismatch.

Back to blog