Unity provides IJob, IJobParallelFor, and IJobParallelForTransform and it turns out these are written in C# so we can learn how they’re implemented. Today’s article goes through each of them so we can learn more about how they work and even see how we can write our own custom job types.

We can see the implementations of IJob, IJobParallelFor, and IJobParallelForTransform by looking at Unity’s open source C# code. To start, let’s look at IJob.cs to see how it’s implemented. The following source code had no comments aside from a legal header and some very long lines, so I’ve added many comments to explain what’s going on and formatted some whitespace for clarity:

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
 
using System;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections.LowLevel.Unsafe;
 
namespace Unity.Jobs
{
    // This is the IJob interface that our job structs implement.
    // This attribute is a hint for the Burst compiler that tells it which
    // struct has the Execute function for this type of job.
    [JobProducerType(typeof(IJobExtensions.JobStruct < >))]
    public interface IJob
    {
        // Job structs must implement everything here. This is called by the
        // Execute function of the struct marked by [JobProducerType] above.
        void Execute();
    }
 
    // Extension functions related to IJob
    public static class IJobExtensions
    {
        // This is the struct marked by [JobProducerType] above. It has the
        // Execute function that's called when the job executes.
        // This is marked internal so code using IJob never sees it.
        internal struct JobStruct<T> where T : struct, IJob
        {
            // This is a pointer to reflection data for the job type. The
            // reflection data lives within the Unity engine and is used by the
            // jobs system to know what this type of job contains.
            public static IntPtr                    jobReflectionData;
 
            // Initializes the job reflection data
            public static IntPtr Initialize()
            {
                // Only create the job reflection data if it's not already
                // created. When creating it, tell it about the C# job type,
                // what type of job (Single in this case) it is, and what
                // delegate to call when the job should execute.
                if (jobReflectionData == IntPtr.Zero)
                    jobReflectionData = JobsUtility.CreateJobReflectionData(
                        typeof(T),
                        JobType.Single,
                        (ExecuteJobFunction)Execute);
                return jobReflectionData;
            }
 
            // Delegate type for the delegate to call when the job should execute
            public delegate void ExecuteJobFunction(
                ref T data,
                IntPtr additionalPtr,
                IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex);
 
            // Function to call when the job should execute
            public static void Execute(
                ref T data,
                IntPtr additionalPtr,
                IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex)
            {
                // Since IJob is a trivial Single type of job, just call the
                // job struct's Execute once with no parameters.
                data.Execute();
            }
        }
 
        // This is the extension function that allows user code to call:
        //   myJob.Schedule();
        // or
        //   myJob.Schedule(myDependency);
        unsafe public static JobHandle Schedule<T>(
            this T jobData,
            JobHandle dependsOn = new JobHandle())
            where T : struct, IJob
        {
            // Create the parameters used when scheduling the job.
            // First, pass a pointer (the address of) to the job struct.
            // Second, pass the reflection data for the job type.
            // Third, pass the dependencies.
            // Fourth, tell it to run the job batched.
            var scheduleParams = new JobsUtility.JobScheduleParameters(
                UnsafeUtility.AddressOf(ref jobData),
                JobStruct<T>.Initialize(),
                dependsOn,
                ScheduleMode.Batched);
 
            // Schedule the job to be run
            return JobsUtility.Schedule(ref scheduleParams);
        }
 
        // This is the extension function that allows user code to call:
        //   myJob.Run();
        unsafe public static void Run<T>(this T jobData) where T : struct, IJob
        {
            // Create the scheduling parameters as above, except tell it to
            // run the job immediately and synchronously instead of batching it.
            var scheduleParams = new JobsUtility.JobScheduleParameters(
                UnsafeUtility.AddressOf(ref jobData),
                JobStruct<T>.Initialize(),
                new JobHandle(),
                ScheduleMode.Run);
 
            // Run the job immediately and synchronously
            JobsUtility.Schedule(ref scheduleParams);
        }
    }
}

This code uses some esoteric Unity APIs and conventions, but as we’ll see it’s mostly boilerplate that can be copied and pasted when creating new types of jobs. To illustrate this, let’s look at the implementation of IJobParallelFor. I’ll again add comments and clean up whitespace, but I’ll limit the comments to just the differences from IJob:

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
 
using System;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections.LowLevel.Unsafe;
 
namespace Unity.Jobs
{
    [JobProducerType(typeof(IJobParallelForExtensions.ParallelForJobStruct < >))]
    public interface IJobParallelFor
    {
        // Notice that this job type has a different signature for its Execute.
        void Execute(int index);
    }
 
    public static class IJobParallelForExtensions
    {
        internal struct ParallelForJobStruct<T> where T : struct, IJobParallelFor
        {
            public static IntPtr                            jobReflectionData;
 
            public static IntPtr Initialize()
            {
                // IJobParallelFor uses JobType.ParallelFor instead of Single
                if (jobReflectionData == IntPtr.Zero)
                    jobReflectionData = JobsUtility.CreateJobReflectionData(
                        typeof(T),
                        JobType.ParallelFor,
                        (ExecuteJobFunction)Execute);
                return jobReflectionData;
            }
 
            // The Execute delegate and function have the same signature as IJob
            public delegate void ExecuteJobFunction(
                ref T data,
                IntPtr additionalPtr,
                IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex);
 
            public static unsafe void Execute(
                ref T jobData,
                IntPtr additionalPtr,
                IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex)
            {
                // Loop until we're done executing ranges of indices
                while (true)
                {
                    // Get the range of indices to execute
                    // If this returns false, we're done
                    int begin;
                    int end;
                    if (!JobsUtility.GetWorkStealingRange(
                        ref ranges,
                        jobIndex,
                        out begin,
                        out end))
                        break;
 
                    // Call the job's Execute for each index in the range
                    for (var i = begin; i < end; ++i)
                        jobData.Execute(i);
                }
            }
        }
 
        unsafe public static JobHandle Schedule<T>(
            this T jobData,
            int arrayLength,
            int innerloopBatchCount,
            JobHandle dependsOn = new JobHandle())
            where T : struct, IJobParallelFor
        {
            var scheduleParams = new JobsUtility.JobScheduleParameters(
                UnsafeUtility.AddressOf(ref jobData),
                ParallelForJobStruct<T>.Initialize(),
                dependsOn,
                ScheduleMode.Batched);
            return JobsUtility.ScheduleParallelFor(
                ref scheduleParams,
                arrayLength,
                innerloopBatchCount);
        }
 
        unsafe public static void Run<T>(this T jobData, int arrayLength)
            where T : struct, IJobParallelFor
        {
            var scheduleParams = new JobsUtility.JobScheduleParameters(
                UnsafeUtility.AddressOf(ref jobData),
                ParallelForJobStruct<T>.Initialize(),
                new JobHandle(),
                ScheduleMode.Run);
            JobsUtility.ScheduleParallelFor(
                ref scheduleParams,
                arrayLength,
                arrayLength);
        }
    }
}

It turns out that not much changed when implementing IJobParallelFor. The only substantial change was in Execute, which was used to implement how this particular type of job works. Finally, let’s look at the last of Unity’s built-in job types: IJobParallelForTransform. I’ll once again mark up the implementation with comments for what’s new and some whitespace cleanup:

// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
 
using UnityEngine;
using System;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
 
//@TODO: Move this into Runtime/Transform folder with the test of Transform component
namespace UnityEngine.Jobs
{
    [JobProducerType(
        typeof(IJobParallelForTransformExtensions.TransformParallelForLoopStruct < >))]
    public interface IJobParallelForTransform
    {
        // Execute's signature has changed again
        void Execute(int index, TransformAccess transform);
    }
 
    public static class IJobParallelForTransformExtensions
    {
        internal struct TransformParallelForLoopStruct<T>
            where T : struct, IJobParallelForTransform
        {
            static public IntPtr                    jobReflectionData;
 
            public static IntPtr Initialize()
            {
                // This is still just a ParallelFor job, not anything special
                // for transforms.
                if (jobReflectionData == IntPtr.Zero)
                    jobReflectionData = JobsUtility.CreateJobReflectionData(
                        typeof(T),
                        JobType.ParallelFor,
                        (ExecuteJobFunction)Execute);
                return jobReflectionData;
            }
 
            // The Execute function signature is also the same
            public delegate void ExecuteJobFunction(
                ref T jobData,
                System.IntPtr additionalPtr,
                System.IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex);
            public static unsafe void Execute(
                ref T jobData,
                System.IntPtr jobData2,
                System.IntPtr bufferRangePatchData,
                ref JobRanges ranges,
                int jobIndex)
            {
                // Make a copy of jobData2
                IntPtr transformAccessArray;
                UnsafeUtility.CopyPtrToStructure(
                    (void*)jobData2,
                    out transformAccessArray);
 
                // Call a couple internal, undocumented functions to get the
                // TransformAccess array
                int* sortedToUserIndex = (int*)TransformAccessArray.GetSortedToUserIndex(
                    transformAccessArray);
                TransformAccess* sortedTransformAccess = (TransformAccess*)TransformAccessArray.GetSortedTransformAccess(
                    transformAccessArray);
 
                // Get the range of sorted indices for this job. Note that this
                // is different than the work stealing range in IJobParallelFor.
                int begin;
                int end;
                JobsUtility.GetJobRange(
                    ref ranges,
                    jobIndex,
                    out begin,
                    out end);
 
                // Call the job's Execute for every index in the range
                for (int i = begin; i < end; i++)
                {
                    // Convert the sorted index to the user index
                    int sortedIndex = i;
                    int userIndex = sortedToUserIndex[sortedIndex];
 
                    // Call the job's Execute with the user index and the
                    // corresponding TransformAccess
                    jobData.Execute(
                        userIndex,
                        sortedTransformAccess[sortedIndex]);
                }
            }
        }
 
        unsafe static public JobHandle Schedule<T>(
            this T jobData,
            TransformAccessArray transforms,
            JobHandle dependsOn = new JobHandle())
            where T : struct, IJobParallelForTransform
        {
            var scheduleParams = new JobsUtility.JobScheduleParameters(
                UnsafeUtility.AddressOf(ref jobData),
                TransformParallelForLoopStruct<T>.Initialize(),
                dependsOn,
                ScheduleMode.Batched);
            return JobsUtility.ScheduleParallelForTransform(
                ref scheduleParams,
                transforms.GetTransformAccessArrayForSchedule());
        }
 
        //@TODO: Run
    }
}

We can see that IJobParallelForTransform is a job just like IJobParallelFor in terms of how it’s scheduled by the job system. The difference lies in its Execute where it uses internal APIs to get the TransformAccess structures. That part is off-limits for our own code, but it shows a bit about how flexible the system is.

Now that we know how each of these job types are implemented, it’s easy to see how we could write our own job types. The bulk of the work is just to fill out an Execute function to do what we want. For inspiration, check out IJobParallelForBatch and IJobParallelForFilter in Unity’s ECS package as of version 0.0.12-preview.8.