How to Create a Rust Plugin for Unreal Engine
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:
<Unreal Engine Root Directory>/Engine/Plugins/
<Project Root Directory>/Plugins/
I used the project root.
- Create a new Unreal C++ project. I used the Top Down starter project with default settings and called it
MyProject
. - In the
MyProject
directory (C:\Users\username\Documents\Unreal Projects\MyProject), add aPlugins
folder.
Create the Rust library
In
Plugins
runcargo new --lib TestPlugin
. Unreal likes to use Pascal case.Open the
TestPlugin
folder.In
cargo.toml
change the package name totest-plugin
.In
cargo.toml
add:[lib] crate-type = ["staticlib"]
This tells cargo to build a
.lib
file.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); } }
Install
cbindgen
:cargo install --force cbindgen
--force
will update it if it’s already installed.In
TestPlugin
add acbindgen.toml
with the following content:autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" namespace = "test_plugin"
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.Run
cargo build --release
to build the project inrelease
mode. You may run into “unresolved external symbol” errors if it’s adebug
build. More on that later.
Add the Unreal wiring
In
TestPlugin
addTestPlugin.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.
In
TestPlugin
addSource/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 toC:\Users\username\Documents\Unreal Projects\MyProject\Plugins\TestPlugin
.ModuleDirectory
points toC:\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
toExternal
. - 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.
In the
MyProject
folder, right click the.uproject
file and click “Generate Visual Studio Project Files”.Open
MyProject.sln
.In
MyProject.Build.cs
(C:\Users\username\Documents\Unreal Projects\MyProject\Source\MyProject\MyProject.Build.cs), append “TestPlugin” toPublicDependencyModuleNames
.PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "NavigationSystem", "AIModule", "Niagara", "EnhancedInput", "TestPlugin" });
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.
- In
MyProjectPlayerController.cpp
add the following includes:#include "Engine/Engine.h" #include "TestPlugin.h"
- 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!