Trying to create readonly array with AtomicSafetyHandle

I’m trying to create a read only NativeArray from a custom container so that any users of the array are required to mark the container as ReadOnly in jobs (and I suppose hopefully prevent any writes in general). It looks like the AtomicSafetyHandle is what I’ll be needing to manipulate to accomplish this but I really don’t understand how to make it work.

Below is a VERY simple example of what I’m trying to do

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;

public class TestGetReadOnly : MonoBehaviour
{
    [NativeContainer]
    public unsafe struct SimpleContainer : IDisposable
    {
        private int* _ptr;
        private int _count;
      
        private AtomicSafetyHandle m_Safety;

        public SimpleContainer(int count)
        {
            _ptr = (int*)UnsafeUtility.Malloc(
                UnsafeUtility.SizeOf<int>() * count,
                UnsafeUtility.AlignOf<int>(),
                Allocator.Persistent);
          
            _count = count;
          
            m_Safety = AtomicSafetyHandle.Create();
        }

        public NativeArray<int> GetReadOnlyArray()
        {
            NativeArray<int> arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<int>(
                _ptr,
                _count,
                Allocator.Invalid);

            NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, m_Safety);
          
            // TODO: I don't know what to do with my safety handle! I don't understand the secondary version if that's
            // what I would use

            return arr;
        }

        public void Dispose()
        {
            UnsafeUtility.Free(_ptr, Allocator.Persistent);
            AtomicSafetyHandle.Release(m_Safety);
        }
    }

    private struct BadJob : IJob
    {
        // Since I don't mark this as ReadOnly I want an exception!
        public NativeArray<int> arr;
      
        public void Execute()
        {
        }
    }

    private void Start()
    {
        SimpleContainer container = new SimpleContainer(10);

        try
        {
            BadJob job = default;
            job.arr = container.GetReadOnlyArray();
      
            // I want this job to throw an exception!
            job.Schedule().Complete();
          
            Debug.Log("BAD! We didn't get an exception :'(");
        }
        catch
        {
            Debug.Log("Good we got an exception!");
            throw;
        }
        finally
        {
            container.Dispose();
        }
    }
}

I want an exception when the job is scheduled since the container isn’t marked as read only. I’m happy to try to figure this out but I’m having a real hard time wrapping my head around how AtomicSafetyHandles work (for example, I don’t get the concept of secondary version at all).

Any help would be greatly appreciated!

1 Like

I managed to stumble around the APIs until I got the behavior I was looking for. What I did was the following…

public NativeArray<int> GetReadOnlyArray()
{
    AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow(m_Safety);
   
    NativeArray<int> arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<int>(
        _ptr,
        _count,
        Allocator.Invalid);

    AtomicSafetyHandle safety = m_Safety;
   
    AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safety);
    AtomicSafetyHandle.UseSecondaryVersion(ref safety);
    AtomicSafetyHandle.SetAllowSecondaryVersionWriting(safety, false);
   
    NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, safety);

    return arr;
}

Though I’m still not confident in my understanding of AtomicSafetyHandles so I wanted to list out my understanding below. If I can have that understanding confirmed or corrected that’d be amazing :slight_smile:

[NativeContainer]
By tagging a container with [NativeContainer] and having a safety handled called m_Safety then jobs will appropriately set my safety handle so that read/write checks will catch and throw if appropriate. Also appears to make sure that that NativeContainers aren’t written to in parallel.

Secondary Versions
Secondary versions are used to give a temporary view into the data. The secondary version can be set to read only or allow writes. If a secondary version is bumped then all existing views are invalidated.

I have noticed, perhaps not surprisingly, if I don’t bump the secondary version but change it from readonly to write allowed then previous readonly views are then changed to allow write.

10 Likes

Thanks for the follow up , it helps a lot !

1 Like