How to implement custom Native List

I am trying to write an editor extension asset that I hope to post to the asset store. For this reason I don’t want it to depend on any preview packages such as the preview Unity.Collections package which contains NativeList (not to be confused with Unity.Collections which is in the core engine by default). But I want to be able to use NativeList functionality. Specifically, I want to be able to add elements to and resize a native collection within a Bursted IJob, which NativeList is able to do. How could I achieve this? I am happy to use unsafe code such as UnsafeUtility.Malloc but I’m not familiar with it. Some example code would be very helpful.

Safe option

Have you considered creating a simple fixed-size list instead? Those could be most compatible, as one can build them out of NativeArrays.

Consider something like this:

( just an untested sketch )

using Unity.Collections;
public struct NativeListFixedSize <T> : System.IDisposable
    where T : unmanaged
{
    NativeArray<T> _data;
    NativeArray<int> _meta;
    public int Capacity => _data.Length;
    int _index { get => _meta[0]; set => _meta[0]=value; }
    public int Length => _index;
    public NativeListFixedSize ( int capacity  , Allocator allocator )
    {
        this._data = new NativeArray<T>( capacity , allocator );
        this._meta = new NativeArray<int>( 1 , allocator );
        this.Clear();
    }
    public bool Add ( T value )
    {
        if( _index < Capacity ) { _data[_index++] = value; return true; }
        else return false;
    }
    /// <remarks> last element in list moves to given position, to fill the gap </remarks>
    public bool RemoveAtFast ( int i )
    {
        if( i<0 || i>=Length ) return false;
            _data *= _data[Length-1];
        _index--;
        return true;
    }
    public void Clear () => this._index = 0;
    public NativeSlice<T> Slice () => this._data.Slice( 0 , this.Length );
    public void Dispose ()
    {
        this._data.Dispose();
        this._meta.Dispose();
    }
    public void GetDataAndDispose ( out NativeArray<T> data )
    {
        data = this._data;
        this._meta.Dispose();
    }
}

side note: Slice() is ideal for iteration and general access without exposing internal fields

var slice = nativeListFixedSize.Slice();
for( int i=0 ; i<slice.Length ; i++ )
	slice *+= 1;

In theory, list can be infinite, but we know it’s not true at all. And this observation can make us think about it more deeply and decide list capacity up front. If that’s done - it’s just a matter of increasing/decreasing an allocated index when adding/removing a value.
But for those cases where list capacity may be an issue you can test:
bool wasCapacityReached = nativeListFixedSize.Length==nativeListFixedSize.Capacity;
and address it by, for example, doubling list’s capacity and running the job again.

Unsafe option

Word of friendly warning first*: pointers can (i.e. will) cause application crashing to desktop, editor included, without even a single warning.

VeryUnsafeList.cs

// src*: https://gist.github.com/andrew-raphael-lukasik/09c8a9c29bb5548ea65273653474f8f1
using UnityEngine;
using UnityEngine.Assertions;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;

public unsafe struct VeryUnsafeList <T> : System.IDisposable where T : unmanaged
{
    public T* ptr;
    public readonly Allocator allocator;
    public int length;
    public int capacity;
    public int size { get; private set; }
    public VeryUnsafeList ( Allocator allocator )
        : this( capacity:0 , allocator:allocator ) {}
    public VeryUnsafeList ( int capacity , Allocator allocator )
    {
        Assert.IsFalse( capacity<0 , "invalid capacity" );
        Assert.IsFalse( allocator==Allocator.Invalid , "invalid allocator" );

        this.length = 0;
        this.capacity = capacity;
        this.allocator = allocator;

        this.ptr = null;
        this.size = 0;
        this.Resize( newCapacity:capacity );
    }
    public T this [ int index ]
    {
        get
        {
            if( index<0 || index>=this.length ) throw new System.IndexOutOfRangeException();
            return this.ptr[index];
        }
        set
        {
            if( index<0 || index>=this.length ) throw new System.IndexOutOfRangeException();
            this.ptr[index] = value;
        }
    }
    public void Add ( T value )
    {
        if( this.capacity==0 )
        {
            this.Resize( 1 );
            Debug.Log($"	resized from {0} to {this.capacity} ({this.size} bytes)");
        }
        if( this.length==this.capacity )
        {
            int old = this.capacity;
            this.Resize( this.capacity * 2 );
            Debug.Log($"	resized from {old} to {this.capacity} ({this.size} bytes)");
        }
        this.ptr[this.length++] = value;
    }
    public void Remove ( T value )
    {
        if( this.length==0 ) return;
        for( int i=0 ; i<this.length ; i++ )
        if( this.ptr*.Equals(value) )
            this.ptr *= this.ptr[--this.length];
    }
    public void RemoveAt ( int index )
    {
        if( index<0 || index>=this.length ) throw new System.IndexOutOfRangeException();
        this.ptr[index] = this.ptr[--this.length];
    }
    public void Clear () => this.length = 0;
    public void Resize ( int newCapacity )
    {
        int newSize = sizeof(T) * newCapacity;
        T* newPtr = (T*) UnsafeUtility.Malloc( size:newSize , alignment:4 , allocator:this.allocator );
        if( this.ptr!=null )
        {
            UnsafeUtility.MemCpy( destination:newPtr , source:this.ptr , size:Mathf.Min(this.size,newSize) );
            this.Dispose();
        }
        this.capacity = newCapacity;
        this.ptr = newPtr;
        this.size = newSize;
    }
    public void Dispose ()
    {
        if( this.ptr!=null )
        {
            UnsafeUtility.Free( this.ptr , this.allocator );
            this.ptr = null;
            this.size = 0;
        }
    }
    public override string ToString () => $"{{ {nameof(ptr)}:{(long)ptr} , {nameof(allocator)}:{allocator} , {nameof(length)}:{length} , {nameof(capacity)}:{capacity} , {(nameof(size))}:{size} }}";
}

AdventuresInUnsafeAllocations.cs

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

using NaughtyAttributes;

public unsafe class AdventuresInUnsafeAllocations : MonoBehaviour
{
    [SerializeField] Allocator mainThreadAllocator = Allocator.Persistent;
    [SerializeField] Allocator jobAllocator = Allocator.Temp;

    void OnEnable () => Run();

    [Button("GO")]
    void Run ()
    {
        VeryUnsafeList<int>* dataPtr = (VeryUnsafeList<int>*) UnsafeUtility.Malloc( size:sizeof(VeryUnsafeList<int>) , alignment:4 , allocator:mainThreadAllocator );
        *dataPtr = new VeryUnsafeList<int>( jobAllocator );

        Debug.Log($"run started");
        var job = new AllocationsJob{ dataPtr = dataPtr };
        job.Schedule().Complete();

        var data = *dataPtr;
        var text = new System.Text.StringBuilder();
        for( int i=0 ; i<data.length ; i++ )
            text.Append( data.ptr *).Append( i<data.length-1 ? ',' : ' ' );
        Debug.Log($"({data.length}) output data: {{ {text} }}");

        data.Dispose();
        UnsafeUtility.Free( dataPtr , mainThreadAllocator );
        Debug.Log($"run completed using {data.allocator} job allocator");
    }
}

public unsafe struct AllocationsJob : IJob
{
    [NativeDisableUnsafePtrRestriction] public VeryUnsafeList<int>* dataPtr;
    public void Execute ()
    {
        (*dataPtr).Add( 999 );
        for( int i=0 ; i<10 ; i++ )
            (*dataPtr).Add( i );
        (*dataPtr).Add( 11 );
        (*dataPtr).Add( -1 );
        (*dataPtr)[11] = (*dataPtr)[12];
        (*dataPtr).Remove( 999 );
        (*dataPtr).RemoveAt( 11 );
    }
}

In that case, make your package.json depend on the first non-preview version of the package.

According to the manual pages, Collections v1.1.0 is the first non-preview version and it is available for Unity 2020.3 LTS.

The solution to this is not adding more needless re-engineering effort but rather to make a sound product design choice, meaning your asset’s minimum required Unity version ought to be 2020.3. Any earlier version wouldn’t make much sense anyway since none of them get any support/updates anymore.