Skip to content

Sharing code between mods

Want mod B to call a function from mod A? Mark it with #[factorio_rs::export], build A, then add A as a normal Cargo dependency. Call it like any Rust crate: provider::greet("hi").

Terminal window
# In the library mod
factorio-rs build
# writes [package.metadata.factorio] + src/factorio_exports.rs (+ lib.rs wiring)
# In the dependent mod (preferred: also merges Factorio.toml deps):
factorio-rs add ../provider
# or Cargo-only (you must add the Factorio dep yourself):
cargo add --path ../provider
factorio-rs build

Working tree: provider / consumer example.

Only items marked with #[factorio_rs::export] are visible to other mods. Plain pub stays private to your transpile (not a Cargo API by itself).

Use pub mod for stage modules you want dependents to see:

pub mod shared;
#[factorio_rs::control]
pub mod control {
#[factorio_rs::export]
pub fn greet(name: &str) {
println!("hello, {name}");
}
}

You can also put #[factorio_rs::export] on a whole mod to export every pub fn inside it.

After factorio-rs build, factorio-rs:

  • Registers control exports with Factorio (remote.add_interface)
  • Keeps shared exports loadable via Lua require
  • Writes [package.metadata.factorio] on your Cargo.toml (so Cargo dependents are discovered)
  • Regenerates src/factorio_exports.rs and wires mod factorio_exports; pub use factorio_exports::*; into lib.rs when missing
Where you export How other mods call it
Control stage (#[factorio_rs::control]) Live call into your mod: remote.call
Shared stage (shared/...) + #[factorio_rs::export] Load your Lua module: require
Shared stage + #[factorio_rs::inline] Same require path (hot helpers; never remote)

Use #[factorio_rs::inline] on pure shared helpers (for example add(a, b)) so dependents bind via require instead of paying remote.call. It implies export and is only valid in shared. Stateful cross-mod APIs stay on control with #[factorio_rs::export].

Optional: rename the Factorio remote interface with #[factorio_rs::export(interface = "math")] or bare #[factorio_rs::export(interface)].

provider/src/shared/api.rs
#[factorio_rs::inline]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// consumer
use provider::shared::api;
let n = api::add(2, 3); // -> require("__provider__/lua/shared/api"); api.add(2, 3)
consumer/Cargo.toml
[dependencies]
provider = { path = "../provider" }
# or a crates.io / git dependency once you publish the package

factorio-rs add ../provider is a thin helper that adds that path dep and merges Factorio.toml dependency strings from the library’s metadata.

Then call:

#[factorio_rs::control]
mod control {
use provider::shared::api;
#[factorio_rs::event(OnSingleplayerInit)]
pub fn on_init() {
provider::greet("world"); // -> remote.call("provider", "greet", ...)
api::greet("world"); // -> require("__provider__/lua/shared/api")
}
}

Notes:

  • Root names like provider::greet are control/remote exports (via factorio_exports.rs).
  • Paths like provider::shared::... are shared/require exports (real modules in the library crate).
  • At transpile time, those calls become remote.call / require - the library’s Rust bodies are not copied into your mod’s Lua.
  • factorio-rs check / build run cargo check on that real library package (and rust-analyzer does the same in the editor).

Non-Rust Factorio deps (DLC, conflicts) still go in Factorio.toml:

[mod]
factorio_version = "2.0"
dependencies = ["? space-age"]

factorio-rs add appends entries like provider >= 0.3.0 from Cargo metadata. A base >= ... line is added automatically when missing.

Publish or path-depend a small crate with empty pub fn stubs and [package.metadata.factorio] - same shape the toolchain writes for factorio-rs libraries.

Problem Fix
library exports missing / no metadata Run factorio-rs build in the library first
provider::greet not found Ensure pub mod control, rebuild the library (regenerates/wires factorio_exports)
Call doesn’t update after changing exports Rebuild the library, then rebuild the dependent