Is it possible to pin pass an int by reference to a method that will then pin that variable in memory so that I can access it with a pointer?
I'm trying to use GCHandle.Alloc to temporarily pin an int in memory so that I can safely store a pointer to that integer. When I try to set the value of the int via the pointer stored in the GCHandle, my integer doesn't get updated. If I create a pointer to the int using unsafe code, I can successfully change the int. It seems like the int is being passed by value, and I"m setting a different int than the one that actually got pinned. Here is an example of the issue I am currently facing:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var count = 8001;
var mc = MyClass.New(ref count);
mc.Process();
}
}
public unsafe sealed class MyClass
{
private GCHandle handle;
private readonly int* countPtr;
private MyClass(GCHandle handle, ref int totalCount)
{
this.handle = handle;
fixed (int* p = &totalCount)
countPtr = p;
}
public void Process()
{
var value = 9001;
var handlePtr = (int*)handle.AddrOfPinnedObject();
*handlePtr = (int)value; // count is unchanged
*countPtr = (int)value; // IT'S OVER 9000!
handle.Free();
}
public static MyClass New(ref int count)
{
var handle = GCHandle.Alloc(count, GCHandleType.Pinned);
return new MyClass(handle, ref count);
}
}
}
The memory addresses of handlePtr and countPtr are different, which leads me to believe that what I think is pinned is not pinned... and I may be building a ticking timebomb into my application.
Use Case:
I have a custom data access layer that uses the following API
public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
var rows = Call()
.SetParameter("PageIndex", pageIndex)
.SteParameter("PageSize", pageSize)
.EnumerateDataSet(commandTimeoutSeconds: 60);
return rows;
}
Call() starts to build a context of how the stored procedure will be setup at runtime. SetParameter and several other methods add data to the context that will be used when the StoredProcedure is executed. EnumerateData sets up an IEnumerable state machine that executes the stored procedure with an SqlDataReader that yields on each row.
Some stored procedures use Offset / Fetch in order to retrieve a limited number of results. In these cases I need to also output the total number of rows using an Sql Output parameter
public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
var count = 0;
var rows = Call()
.SetParameter("PageIndex", pageIndex)
.SteParameter("PageSize", pageSize)
.OutputTotalCount(x=> count = x)
.EnumerateDataSet(commandTimeoutSeconds: 60)
.ToList();
totalCount = count;
return rows;
}
The Action I am passing will be executed after all the rows are enumerated and the SqlDataReader is closed, as this is the only way to get an OutPutParameter from an SQL Stored Procedure. As such the only reason that this works is because I am calling .ToList(). If I don't call .ToList() the out totalCount gets set to 0, the method returns and Action to set count gets fired after the reading through all the rows.
I'm looking to add an additional OutputTotalCount overload in order to be able to use do
public IEnumerable<Rows> GetAllData()
{
var count = 0;
return CallMyProcedure(1, int.MaxValue, out count);
}
public DataPage GetDataPage(int pageIndex, int pageSize)
{
var count = 0;
var list = CallMyProcedure(pageIndex, pageSize, out count).ToList()
return new DataPage
{
Data = list,
TotalCount = count
}
}
public IEnumerable<Rows> CallMyProcedure(int pageIndex, int pageSize, out int totalCount)
{
totalCount = 0; // Needed because we are leaving this method before setting
var rows = Call()
.SetParameter("PageIndex", pageIndex)
.SteParameter("PageSize", pageSize)
.OutputTotalCount(ref totalCount)
.EnumerateDataSet(commandTimeoutSeconds: 60)
return rows;
}
The new OutPutTotalCount overload here essentially adds an Instance of MyClass to the context that EnumerDataSet will use to execute the stored procedure when GetEnumerator is called. Process will be called after all the rows have been read. The GCHandles are also tracked by the execution context and are free'd inside of a try/finally.