How to Create Custom Function Libraries Using ExecutableFunctionLibrary in Ceres
To create custom function libraries using ExecutableFunctionLibrary, define a partial class inheriting from ExecutableFunctionLibrary, annotate static methods with [ExecutableFunction], and let the Ceres source generator handle runtime registration automatically.
The Ceres visual scripting system for Unity allows you to expose C# methods as executable nodes in Flow graphs. By creating custom function libraries with ExecutableFunctionLibrary, you can extend the node palette with domain-specific utilities while maintaining high-performance function pointer invocation.
Understanding ExecutableFunctionLibrary Architecture
ExecutableFunctionLibrary serves as the abstract base class that bridges static C# methods with Ceres Flow's runtime execution engine. The architecture relies on a compile-time source generation pattern to eliminate reflection overhead at runtime.
Core Components
The implementation spans three primary components in the akikurisu/ceres repository:
ExecutableFunctionLibrary.cs– Located atRuntime/Flow/Models/Libraries/ExecutableFunctionLibrary.cs, this abstract base provides theCollectExecutableFunctions()method and registration infrastructure.ExecutableLibraryGenerator.cs– The Roslyn source generator atRuntime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibraryGenerator.csthat scans for[ExecutableFunction]attributes and emits partial class implementations.ExecutableReflection<T>– The runtime lookup table that maps function names to registered delegates, enablingFlowNode_ExecuteFunctionnodes to resolve and invoke methods efficiently.
Step-by-Step Implementation Guide
1. Declare the Partial Class
Create a partial class that inherits from ExecutableFunctionLibrary. The partial keyword is mandatory because the source generator will produce a companion file implementing CollectExecutableFunctions().
using Ceres.Graph.Flow;
using Ceres.Annotations;
[CeresGroup("Gameplay")]
public partial class GameplayFunctionLibrary : ExecutableFunctionLibrary
{
// Methods will be defined here
}
2. Annotate Methods with [ExecutableFunction]
Mark any static method you want to expose with the [ExecutableFunction] attribute. These methods become available as nodes in the Flow graph editor.
[ExecutableFunction]
public static float CalculateDistance(Vector3 a, Vector3 b)
{
return Vector3.Distance(a, b);
}
Optional attribute parameters control execution behavior:
ExecuteInDependency– Marks the function as a data retrieval operation that must complete before dependent nodes execute.IsScriptMethod– Indicates the function operates on a target instance.IsSelfTarget– Automatically passes the graph container as the first parameter.
3. Source Generation and Runtime Registration
During compilation, the source generator analyzes your partial class and generates a hidden file that:
- Overrides
CollectExecutableFunctions() - Calls
RegisterExecutableFunctionPtr<T>(name, paramCount, functionPtr)for each annotated method - Registers file-line metadata for debugging
When you instantiate your library (new GameplayFunctionLibrary()), the base constructor invokes the generated CollectExecutableFunctions() implementation. This registers each function pointer with ExecutableReflection<TLibrary>.RegisterStaticExecutableFunctionPtr, as implemented in ExecutableFunctionLibrary.cs lines 33-36.
Practical Code Example
The following complete example demonstrates advanced configurations including dependency execution, self-targeting methods, and overloaded functions with labels:
using Ceres.Graph.Flow;
using Ceres.Graph.Flow.Annotations;
using Ceres.Annotations;
using UnityEngine;
/// <summary>
/// Custom library exposing gameplay utilities to Flow graphs.
/// </summary>
[CeresGroup("Gameplay")]
public partial class GameplayFunctionLibrary : ExecutableFunctionLibrary
{
// Simple utility – expose Vector3 distance
[ExecutableFunction]
public static float Flow_CalculateDistance(Vector3 a, Vector3 b)
{
return Vector3.Distance(a, b);
}
// Data-retrieval function that must run before dependent nodes
[ExecutableFunction(ExecuteInDependency = true)]
public static Vector3 Flow_GetPlayerPosition()
{
var player = GameObject.FindGameObjectWithTag("Player");
return player != null ? player.transform.position : Vector3.zero;
}
// Instance-style method on a target GameObject
[ExecutableFunction(IsScriptMethod = true, IsSelfTarget = true)]
public static Component Flow_GetComponent(GameObject target, Component dummy)
{
// IsSelfTarget automatically passes the graph container as target
return target.GetComponent(dummy.GetType());
}
// Overload example – use CeresLabel to differentiate in the UI
[ExecutableFunction, CeresLabel("Log Message")]
public static void Flow_Log(string msg) => Debug.Log(msg);
[ExecutableFunction, CeresLabel("Log Message with Color")]
public static void Flow_Log(string msg, Color col) =>
Debug.Log($"<color=#{ColorUtility.ToHtmlStringRGB(col)}>{msg}</color>");
}
The source generator produces an auto-generated partial file that you should not edit manually, following the pattern documented in flow_function_library.md.
Key Configuration Attributes
Beyond basic exposure, several attributes refine how functions appear and behave in the Flow editor:
[CeresGroup(string)]– Applied to the class level to categorize the library in the node palette (e.g.,[CeresGroup("Gameplay")]).[CeresLabel(string)]– Differentiates overloaded methods by providing distinct display names in the UI.[ExecutableFunction(ResolveReturn = true)]– Configures return value handling for specific execution contexts.
For production libraries, follow the best practices outlined in the official documentation: keep parameter counts at six or fewer, guard against null references, and use descriptive method names prefixed with domain identifiers (e.g., Flow_ or Gameplay_).
Summary
- Create a partial class inheriting from
ExecutableFunctionLibraryto enable source generator integration. - Apply [ExecutableFunction] to static methods you want to expose as Flow nodes.
- The Ceres source generator automatically creates the registration boilerplate by overriding
CollectExecutableFunctions(). - At runtime, the library constructor registers function pointers via
ExecutableReflection<T>, making methods available toFlowNode_ExecuteFunctionnodes. - Use [CeresGroup] for library categorization and [CeresLabel] to handle method overloads gracefully.
Frequently Asked Questions
Why must the class be declared as partial?
The partial keyword is required because ExecutableLibraryGenerator.cs emits a companion file during compilation that implements the CollectExecutableFunctions() method. This generated code registers each [ExecutableFunction] method with the runtime system using RegisterExecutableFunctionPtr. Without the partial modifier, the compiler cannot merge your handwritten code with the generated registration logic.
How does the Flow engine resolve which function to execute?
When a FlowNode_ExecuteFunction node executes, it queries ExecutableReflection<TTarget>.GetFunction using the function name, static/instance flag, and parameter count. This lookup retrieves the function pointer registered during library instantiation, allowing direct invocation without reflection overhead.
Can I create libraries for instance methods rather than static methods?
While ExecutableFunctionLibrary primarily exposes static methods, you can simulate instance behavior using the IsScriptMethod and IsSelfTarget parameters. When IsSelfTarget is true, the Flow system automatically passes the graph's container GameObject as the first parameter, enabling you to write methods that operate on specific instances while maintaining static signatures for registration.
Where can I find reference implementations of custom libraries?
The repository includes UnityExecutableLibrary.cs at Runtime/Flow/Models/Libraries/UnityExecutableLibrary.cs, which demonstrates best practices for exposing Unity engine utilities. Additionally, the documentation at Documentation~/docs/flow_function_library.md provides comprehensive attribute references and troubleshooting guidance for complex scenarios like generic methods or async operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →