Coding Babble  Code it once more, with feeling.

How to Create a Rust Plugin for Unreal Engine

By Luke Simpson

Introduction

In this post I’ll explain how to call Rust code in Unreal Engine. The idea is to build a static library, use cbindgen to autogenerate a C/C++11 header file, and load it up using Unreal’s plugin system.

Caveat

You may have a more difficult time getting Rust working on consoles, though I hear it’s possible. Also, it sounds like extra work is needed to use std, so you may want to go with a no_std strategy.

Create the Unreal project

There are two places plugins can exist where UE5 will automatically find them:

  1. <Unreal Engine Root Directory>/Engine/Plugins/
  2. <Project Root Directory>/Plugins/

I used the project root.

  1. Create a new Unreal C++ project. I used the Top Down starter project with default settings and called it MyProject.
  2. In the MyProject directory (C:\Users\username\Documents\Unreal Projects\MyProject), add a Plugins folder.

Create the Rust library

  1. In Plugins run cargo new --lib TestPlugin. Unreal likes to use Pascal case.

  2. Open the TestPlugin folder.

  3. In cargo.toml change the package name to test-plugin.

  4. In cargo.toml add:

    [lib]
    crate-type = ["staticlib"]
    

    This tells cargo to build a .lib file.

  5. Replace src/lib.rs with the following content:

    #[no_mangle]
    pub extern fn add_5(num: u32) -> u32 {
       num + 5
    }
    
    #[cfg(test)]
    mod tests {
       use super::*;
    
       #[test]
       fn it_works() {
          let result = add_5(2);
          assert_eq!(result, 7);
       }
    }
    
  6. Install cbindgen:

    cargo install --force cbindgen
    

    --force will update it if it’s already installed.

  7. In TestPlugin add a cbindgen.toml with the following content:

    autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
    namespace = "test_plugin"
    
  8. Now generate the C header file:

    cbindgen --config cbindgen.toml --crate test-plugin --output Source/TestPlugin/Public/TestPlugin.h
    

    This creates a header file in Source/TestPlugin/Public, the default location for header files.

  9. Run cargo build --release to build the project in release mode. You may run into “unresolved external symbol” errors if it’s a debug build. More on that later.

Add the Unreal wiring

  1. In TestPlugin add TestPlugin.uplugin with the following content:

    {
       "FileVersion": 3,
       "Version": 1,
       "VersionName": "1.0",
       "FriendlyName": "Test Plugin",
       "Description": "",
       "Category": "Other",
       "CreatedBy": "",
       "CreatedByURL": "",
       "DocsURL": "",
       "MarketplaceURL": "",
       "SupportURL": "",
       "CanContainContent": true,
       "IsBetaVersion": false,
       "IsExperimentalVersion": false,
       "Installed": false
    }
    
    • We need this file so that when we “Generate Visual Studio Project Files” in a later step, our plugin will show in the solution.
    • If you generate a new C++ plugin from within the Unreal Editor, it will also add a section called “Modules”, however we can omit it. It’s only needed if your plugin is a C++ module.
  2. In TestPlugin add Source/TestPlugin/TestPlugin.Build.cs with the following content:

    using UnrealBuildTool;
    using System.IO;
    
    public class TestPlugin : ModuleRules
    {
       public TestPlugin(ReadOnlyTargetRules Target) : base(Target)
       {
          PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
          Type = ModuleType.External;
    
          string lib = Path.Combine(PluginDirectory, "target\\release\\test_plugin.lib");
          string includes = Path.Combine(ModuleDirectory, "Public");
    
          PublicAdditionalLibraries.Add(lib);
          PublicIncludePaths.Add(includes);
       }
    }
    
    • With this folder structure, Unreal will find our build file automatically.
    • UseExplicitOrSharedPCHs ensures our module complies with Unreal’s IWYU conventions.
    • PluginDirectory points to C:\Users\username\Documents\Unreal Projects\MyProject\Plugins\TestPlugin.
    • ModuleDirectory points to C:\Users\username\Documents\Unreal Projects\MyProject\Plugins\TestPlugin\Source\TestPlugin\.
    • Normally, Unreal tries to build C++ modules when you build your project. Since we don’t want it to build anything, we set the Type to External.
    • We also tell Unreal where our static library and C header files are. In the future, you could get fancy with this and use Target.Platform to point to a different library file depending on the platform.
  3. In the MyProject folder, right click the .uproject file and click “Generate Visual Studio Project Files”.

  4. Open MyProject.sln.

  5. In MyProject.Build.cs (C:\Users\username\Documents\Unreal Projects\MyProject\Source\MyProject\MyProject.Build.cs), append “TestPlugin” to PublicDependencyModuleNames.

    PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "NavigationSystem", "AIModule", "Niagara", "EnhancedInput", "TestPlugin" });
    
  6. Rebuild the project.

Calling our Rust code from a C++ class

At this point, you should be able to type #include "TestP in one of your source files and IntelliSense should list your header file you generated earlier. Note that I was having issues with IntelliSense on one of my attempts. You may have to reopen the solution and/or file. The real test is if you can include the header file and build the project successfully.

  1. In MyProjectPlayerController.cpp add the following includes:
    #include "Engine/Engine.h"
    #include "TestPlugin.h"
    
  2. In the BeginPlay method, add the following:
       if (GEngine)
       {
          int num = test_plugin::add_5(3);
          FString msg = FString::FromInt(num);
          GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, *msg);
       }
    

Now when you build and run the game, you should see a glorious 8 appear on screen.

Fixing unresolved external symbols

If you use std, you may see errors like the ones below when you try to build your Unreal project:

1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_GetUserProfileDirectoryW referenced in function _ZN3std3env8home_dir17h01329e00848f730aE
1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_NtCreateFile referenced in function _ZN3std3sys7windows2fs20open_link_no_reparse17hee3358b6bcfc697eE
1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_RtlNtStatusToDosError referenced in function _ZN3std3sys7windows2fs20open_link_no_reparse17hee3358b6bcfc697eE
1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_NtReadFile referenced in function _ZN3std3sys7windows6handle6Handle16synchronous_read17h4a41a152df556564E
1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_NtWriteFile referenced in function _ZN3std3sys7windows6handle6Handle17synchronous_write17h4888e2c67110ede7E
1>test_plugin.lib(std-e493bcbfdc66a475.std.9ab95dd99822253f-cgu.0.rcgu.o) : error LNK2019: unresolved external symbol __imp_BCryptGenRandom referenced in function _ZN3std3sys7windows4pipe9anon_pipe17h0951b5613efec006E

These methods are from system libraries. To find out what system libraries your crate uses, run this command:

cargo rustc -q -- --print=native-static-libs

-q = “Do not print cargo log messages”

The output should look something like this:

note: Link against the following native artifacts when linking against this static library. The order and any duplication can be significant on some platforms.

note: native-static-libs: kernel32.lib advapi32.lib bcrypt.lib kernel32.lib ntdll.lib userenv.lib ws2_32.lib kernel32.lib ws2_32.lib kernel32.lib ntdll.lib kernel32.lib msvcrt.lib

Now that we know the names, we can add them to TestPlugin.Build.cs, and Unreal will check the default paths to find them.

PublicSystemLibraries.Add("kernel32.lib");
PublicSystemLibraries.Add("advapi32.lib");
PublicSystemLibraries.Add("bcrypt.lib");
PublicSystemLibraries.Add("kernel32.lib");
PublicSystemLibraries.Add("ntdll.lib");
PublicSystemLibraries.Add("userenv.lib");
PublicSystemLibraries.Add("ws2_32.lib");
PublicSystemLibraries.Add("kernel32.lib");
PublicSystemLibraries.Add("ws2_32.lib");
PublicSystemLibraries.Add("kernel32.lib");
PublicSystemLibraries.Add("ntdll.lib");
PublicSystemLibraries.Add("kernel32.lib");
PublicSystemLibraries.Add("msvcrt.lib");
PublicAdditionalLibraries.Add(lib);

Your project should now build successfully.

Support the blog! Buy a t-shirt or a mug!

Tags: