Adding Third-Party Libraries to Unreal Engine : NASA's SPICE Toolkit (Part 2)

post-thumb

Note: This is the 2nd post of a 5 part series.

Previous Post : Adding Third-Party Libraries - Part 1

Goal:

Integrate NAIF’s C-SPICE toolkit and UE5

Let’s take a brief look at the toolkit we are integrating, NASA/NAIF’s C-SPICE toolkit. In particular, the directory structure.

cspice/
    data/
    doc/
    etc/
    exe/
+   include/
    lib/
+       cspice.lib
    src/
        brief_c/
        ...
-       cspice/
        ...
    lib/

CSPICE Toolkit directory structure

We really only need two things from the SPICE toolkit:

  • lib/cspice.lib
  • include/

lib/cspice.lib
This is the library containing all the compiled code we need. There’s another library (csupport.lib) but mostly it’s just a helper library for the standalone CSPICE executable utility apps. (Which, we won’t be using here.)

include/
And of course we’ll need access to the C header files to actually call the library. And that’s really all we need.

Possible Solutions

There are a few alternate approaches we can consider.

  • Integrate the CSPICE pre-built static library into our project.
  • Put the pre-built library into a DLL and add that as a project dependency.
  • Re-build the static library ourselves outside of UE and link to that.
  • Re-build the static library via the UE build system
  • Just put all the CSPICE source code into a UE module and link directly to it.

We’re going to discuss all of these options. Who knows, maybe your project will need an approach we don’t implement in this series, so let’s touch on them all.

The CSPICE static library

NAIF supplies a pre-built static library for a few relevant platforms - Win32, Win64, Mac. The nice thing about using the library is it comes pre-built and tested/validated by NAIF. That exact library is used to drive real-life space missions. All that is highly desirable.

And, think about it for a sec. We’re marrying a very powerful toolkit (SPICE) to a very powerful authoring platform (Blueprints). Blueprints separate the concerns of programming (C, IDL, MATLAB, etc), from domain (space flight, orbital mechanics). In other words, this will empower domain experts in space flight etc to solve problems without writing code, at all. All kinds of people who wouldn’t touch C or IDL or MATLAB or UE C++ or FORTRAN or Java etc etc could solve these problems by stringing nodes together in UE Blueprints. Dayuuuuum! That’s beyond cool, it’s sexy! People could do that. I hope someone does it. It would be sucky for us to give them something that doesn’t perform as then normally expect from SPICE.

If only Blueprints ran on an iPad - think about that in this context :-D.

The union of SPICE and UE5 will be a very powerful visualization platform for those rendering cinematics. If you have the meshes, textures, and data kernels, you should be able to setup a cinematic to render something like the James Webb Space Telescope’s journey from launch to L2 in like 5 minutes.

So, yes, let’s just integrate the NAIF pre-built Win64 library that comes with the Toolkit into UE and call it good (at least for Win64 builds).

Spice Toolkit info


Option 1 : Integrate the CSPICE pre-built static library

To do this, we’re going to need to poke around in the UnrealBuildTool code a bit, there’s really no way to do this kind of stuff without doing so. Isn’t source-available software great? In a default install of Unreal Engine 5 (Early Access), the build tool source is located here:

C:\Program Files\Epic Games\UE_5.0EA\Engine\Source\Programs\UnrealBuildTool

So, open up UnrealBuildTool.sln and take a peek around :).

The parts we’ll want to pay the closest attention to are TargetRules.cs and ModuleRules.cs These are the base classes for the project and module specific *.Target.cs and *.Build.cs build system files in our C++ Projects and Modules.

TargetRules.cs
Of course, this is where a lot of the Project specific configuration is set. Browsing the source lets us see all the config points we don’t normally see, the stuff we don’t normally override. If we want to know what something does, we can search the code for it and follow the implementation.

ModuleRules.cs
is the same thing, but for each module in your C++ project. A couple of things are interesting here. And, they’re needed for this solution to fly.

ModuleRules::PublicIncludePaths
We can add a path to the CSPICE header files here, and then other modules will be able to find the CSPICE header files.

ModuleRules::PublicDefinitions
If we need to add any preprocessor definitions for OTHER modules to compile against, they go here.

*.TargetRules.cs informs the UE build system determine how to build the project as a whole for a given target (platform).

*.ModuleRules.cs exists for each module in the project. The build system uses this file to determine how to build the module.

If you take a peek in the CSPICE makefiles you’ll see the library was built with a couple of things defined. We will need to include the correct definitions when we compile code against the CSPICE include files. Imagine if, say, a struct layout depended on the preprocessor definitions. We see we can add these to PublicDefinitions so other modules that include CSPICE headers will compile correctly. Let’s see how the cspice lib was built, look in cspice\src\cspice\mkprodct.bat and we see:

set cl= /c /O2 -D_COMPLEX_DEFINED -DMSDOS -DOMIT_BLANK_CC -DNON_ANSI_STDIO

rem 
rem  The optimization algorithm has a very tough time with zzsecptr.c,
rem  so exempt this routine from optimization.
rem 

rename zzsecprt.c zzsecprt.x

rem
rem  Compile everything else.
rem

for %%f in (*.c) do cl %%f 

rem
rem  Set the cl variable to omit optimization.  Compile zzsecprt.c.
rem 

set cl= /c -D_COMPLEX_DEFINED -DMSDOS -DOMIT_BLANK_CC

Interesting. One file in the library gets compiled differently than the others. Later, we’ll check the makefile for one of the CSPICE programs that links to the library… We’ll include the same preprocessor definitions the program’s project uses when we link to the library. And, they’ll be exposed to our UE5 modules via PublicDefinitions.

ModuleRules::Type
This is an important one. If Type is set to CPlusPlus, Unreal Engine is going to look around for some code to build here. In this case all we have is a library, so we want it set to External. That instructs the UE5 build system to simply link the relevant libraries.

ModuleRules::PublicAdditionalLibraries
Finally, this is where we tell Unreal Engine where the CSpice Library is.

So, maybe this is our CSpice_Library.Build.cs file:

using System.IO;
using UnrealBuildTool;

public class CSpice_Library : ModuleRules
{
    public CSpice_Library(ReadOnlyTargetRules Target) : base(Target)
    {
        bAddDefaultIncludePaths = false;

        string cspiceDir = Path.Combine(ModuleDirectory, "cspice/");
        string includeDir = Path.Combine(cspiceDir, "include/");
        string libFile = CSpiceLibPath(Target);

        if (Target.Platform == UnrealTargetPlatform.Win64)
        {
            PublicDefinitions.Add("_COMPLEX_DEFINED");
            PublicDefinitions.Add("MSDOS");
            PublicDefinitions.Add("OMIT_BLANK_CC");
            PublicDefinitions.Add("NON_ANSI_STDIO");
        }

        Type = ModuleRules.ModuleType.External;
        PublicAdditionalLibraries.Add(libFile);

        if (!Directory.Exists(includeDir))
        {
            string Err = string.Format(
                "cspice headers not found at {0}.  A copy of cspice/include must be present",
                 includeDir
                 );

            System.Console.WriteLine(Err);
            throw new BuildException(Err);
        }

        PublicIncludePaths.Add(includeDir);
    }

    static public string CSpiceLibPath(ReadOnlyTargetRules targetRules)
    {
        string relativePathToCSpiceLib = "Source\\CSpice_Library\\lib\\" 
            + targetRules.Platform.ToString() 
            + "\\cspice.lib";

        return Path.Combine(targetRules.ProjectFile.Directory.FullName, relativePathToCSpiceLib);
    }
}

The problem with this solution…

If we go down this route we have potential C Runtime Library compatibility issues. If the library was built with one CRT version and then linked to a different CRT version, there could be a problem. And, this is in fact the case. The SPICE libraries were built using pre-VS2015 CRT header files. We’re trying to link it to a post-VS2015 CRT Lib.

So, this option has turned into…:

Option 1 (redefined): Integrate the CSPICE pre-built static library with fix ups for CRT header file / library file incompatibilities

We would see 3 unresolved external symbols if we link the NAIF library against a post VS2015 CRT library:
fprintf
sprintf
__iob_func

To handle the first two, we can fix it by adding this to our CSpice_Library.Build.cs file:

PublicSystemLibraries.Add("legacy_stdio_definitions.lib");

This will include a library the defines a few of the missing symbols including fprintf and sprintf.

But, for __iob_func we have a real problem. There’s various code snippets floating around that supposedly fix this - and they’re ALL wrong. See the answer from MarkH here.

Okay, so does the SPICE toolkit actually need this symbol anyways? Is it important that our fix actually work, or just that we can get it to link successfully? Well, yes it is referenced, or it wouldn’t generate an unresolved symbol error from the linker. In fact, if you employ any of the CRT incompatibility hacks floating around our app will crash. In this case Windows will throw an error any time SPICE tries to read a SPICE kernel file . The hacks are that - hacks, and hacks are just not safe. This path is at a dead end. Using the static lib pre-built by NAIF is a non-starter.

Incidentally, the jme5297/UnrealSpice project on github employs this solution. It was built 4 years ago though, apparently before the CRT compatibility issue surfaced. It made perfect sense at the time. This would have been the best option, except for the CRT issue on Windows.


Option 2 : Build a DLL from the pre-built library

This is an option. Microsoft suggests it as a possible solution for the CRT incompatibility issue discussed above. And, if we look through the UnrealBuildTool source there appears to be good support for it.

In this case we expose the CSPICE headers to our other module the same as above, via PublicIncludePaths.

There are then three sub-options here. All involve a building a DLL which is in turn linked to the CSPICE static library. After building the wrapper we would have a cspice_wrapper.dll and a matching stub static library, cspice_wrapper.lib which we can implicitly link to.

Sub-options:

For our first option, Windows will load our DLL when the .exe starts up. To do this, we link to the static stub library, cspice_wrapper.lib. Viola.

2b: Delay Load an implicitly linked DLL

ModuleRules::PublicDelayLoadDLLs
Our second option is to delay-load the DLL. E.g., the DLL is not automatically loaded until it is actually called. But, the DLL is still implicitly linked to a static library stub cspice_wrapper.lib via PublicDelayLoadDLLs.

2c: Load Library

The third option is to call LoadLibrary and GetProcAddress ourselves at runtime to access the DLL’s functionality. Certainly an option. It would platform-specific code right there in our cpp code, though, as opposed to abstracting the differences away in our build system. Anyways, it could be a bit of work to manage all this.

One benefit of this option, though, would be that each instance of the loaded DLL could be reinitialized by re-loading. CSPICE has a lot of persistent data. Things stick around in memory, potentially affecting future library calls. We could unload/reload the DLL. It would be nice if we could load multiple instances of the DLL into memory, each with its own heap, etc so we could have multiple virtual CSPICE sessions — but we can’t. Windows does not support such a use case. Sad face, sad clown.

However… A further pitfall to this approach is that the DLL could become separated from the app and there’s not much there to catch it. UE doesn’t even know the app depends on this DLL. Will it be there when the time comes? How do we ensure it? What do we do if we can’t find the library? This is a potential issue with implicit linking as well, but at least in that case Windows can detect the missing DLL at loadtime and handle it…

Option 2 (DLL) appears to be viable by all sub-3 options. The biggest con is that the dynamicness does increase runtime complexity. We might need to handle missing or incompatible DLL versions, etc. There are also security issues to think through, the DLL dependency is a potential attack vector.


Option 2.5: EXTENDED REMIX!

CSPICE running as a separate process

<TL;DR WARNING>

There’s a less obvious option here. We could extend the DLL idea further. A DLL loads the spice code into a wholly different module from our compiled code. What about putting the code into a wholly separate process?

Been there, done that, when integrating the SPICE module with Unity3D. No, not THIS SPICE library. THIS SPICE library. In this case, SPICE is an acronym for “Simulated Program with Integrated Circuit Emphasis”.

Anyways. I integrated e-SPICE (electronics) by linking some interprocess communications code to the library into a new .exe/process.

Integration of toolkit as separate processes (instances spawn on demand)

Why did I integrate I go to all the trouble that involved?

You’ve asked a great question! BECAUSE THE EFFING THING CRASHES ALL THE TIME. It is LESS annoying to go to the trouble to slap it into a different process and write a communications API on both sides than to have your game editor crash every 10 minutes.

e-SPICE left a bunch of dirty state hanging around too that contributed to the crashiness. This is where the DLL/LoadLibrary model above could shine. The Unity plugin system loads the DLL once, when the editor starts. What if that’s the DLL that you’re iterating on? PITA. Shutdown and restart the editor to see your changes. OR. You can load the DLL yourself via LoadLibrary when you enter playmode, and unload it when you bounce back to the editor. MUCH easier workflow.

If this s-SPICE (spacecraft) library were in a separate process we could even have multiple virtual “instances” of Spice. The s-SPICE library could be implemented as a UE Object, for each object we spawn a new process that it communicates to - sandboxing each instance.

The NAIF Spacecraft SPICE library is really really really robust, unlike e-SPICE however. So is it worth the complexity tradeoff? Interprocess communication layers were exceptionally easy to slap together for Windows using Named Pipes. We had a few interprocess tools on the Direct3D team (stress tests, etc) and used Named Pipes for them. But, the communications layer adds a complexity that is bound to become a liability.

Named Pipe servers turned out to be security nightmares so Microsoft eventually effectively disabled them (you bastards!).

Dang you, Microsoft!:

Remarks Windows 10, version 1709: Pipes are only supported within an app-container; ie, from one UWP process to another UWP process that’s part of the same app. Also, named pipes must use the syntax “.\pipe\LOCAL” for the pipe name.

Can you imagine coming in to work to find a Windows update broke your app, all your tools, and having to sort that out and fix it by migrating to WinSockets or something?

<\TL;DR>


Option 3 : Re-build the static library ourselves.

The CSPICE Toolkit comes with a build that works from the command line. It works straight out of the box for Windows, Mac, Linux.

We can thusly use the exact same build tooling (and thus compiler options, preprocessor definitions, etc), that NAIF used to build the original libraries.

So, why not manually rebuild the library ourselves and link to that, as in option 1 above?

This option becomes Option 1 redux, minus the CRT incompatibility issues.

Your 6th sense (“engineering sense”) is probably tingling though. It’s a very brittle solution. A long time in the future, like a long LONG time in the future, like maybe 3 months from now there might be a new platform. Maybe Win128 or “Xbox 0” or something. And that library’s going to need rebuilt. If you’re still there, will you still remember how to build it? What if you realllly need to insert a tiny change to CSPICE and rebuild, at some point? What if *gasp* you’re no longer there because you won American Idol (again) and you’re on a warm beach somewhere downing umbrella drinks as fast as they come?

Okay, so this feels like - on a technical level - the end result - it’s a decent solution. We have a new static library built exactly per the NAIFs build system and recommendations. But, on a practical level, for future maintenance etc, it’s a time bomb looking to go BOOM.


Option 4 : Re-build the lib as above using NAIF build scripts, but invoked from our project’s build

We’re starting to get close here. This has the benefits of using the NAIF build system, just as the original libs were built… And we can automate building the library so it can be rebuilt for new platforms etc etc. And the system itself documents the build process, for purposes of future maintenance.

This path is viable. In fact, it’s the solution we’re going to implement. We’ll punt any further discussion about this option into the next part.


Option 5 : Build CSPICE source code directly into a module using the UE build tooling

It feels a bit anti-climatic to discuss right after we just said we’re doing something else, doesn’t it?

The main drawback is - the code isn’t building with the exact same compiler options etc as the NAIF build.

Remember this snippet, from above? The comments probably got your attention, yes?

rem 
rem  The optimization algorithm has a very tough time with zzsecptr.c,
rem  so exempt this routine from optimization.
rem 

rename zzsecprt.c zzsecprt.x

The compiler command line is different for zzsecprt.c versus the other .c files. How are you going to swing that? The build script comments should give you pause that this library is a bit sensitive to the build config, yes? You don’t really want to see the first crewed mission to mars end up at the wrong planet just because you built a shoddy version of a NASA developed library, which then spread into the science community because, well, UE Blueprints, … DO YOU?

There are a few other issues here, a few minor things to figure out. Sorting through them was mostly straight-forward for UE4, but I never did get it working perfectly with UE5. The build tool had meaningful differences between UE4 and UE5, I tracked the issues into some of those changes*… Best to detangle building the SPICE code from the rest of our project. Separate concerns are best conquered separately.

This option is more of a quick-exploration trial-type solution. And if you’re just tinkering it’s probably not worth the investment of option 4 above. Option 3 is probably your best bet, but if you’re modifying the CSPICE code yourself a lot while tinkering around, sure, this seems reasonable. Ish.

In Part 3 of this series we’ll drill down on Option 4.

Next Post : Adding Third-Party Libraries - Part 3

* The last remaining issues would have been fixed by applying THIRD_PARTY_INCLUDES_START/THIRD_PARTY_INCLUDES_END appropriately, as they related to a few cases of warnings treated like errors. The UE5 build system changed how warnings are handled and silenced. However, I hadn’t noticed these macros before - they are documented in the Unreal Engine documentation on ThirdPartyLibraries.

Comments are disabled. To share feedback, please send email, or join the discussion on Discord.