Pointers

Top  Previous  Next

Pretranslated C# code > Pointers

 

Overview

 

Delphi2C# includes a small pointer runtime that helps translated Delphi code preserve common Delphi pointer semantics in managed C#. The runtime supports constructs such as:

 

`Pointer` and untyped pointer parameters
typed pointers such as `PInteger` and `PMyRecord`
`PByte`
`PChar` and `PWideChar`
`GetMem`, `AllocMem`, and `FreeMem`
`FillChar` and `Move`
record pointers
pointer arithmetic
pointer views over arrays, dynamic arrays, strings, and native memory
temporary pinning for native API calls

 

 

C# does not provide Delphi-compatible managed pointer types. Delphi2C# therefore uses small pointer structs that simulate the most important Delphi operations while retaining bounds checks and managed-memory integration where possible.

 

The pointer runtime is a compatibility layer for translated Delphi code. It is not intended to replace normal C# arrays, spans, strings, references, or safe handles in newly written C# code.

 

 

Pointer values and backing storage

 

The runtime separates a pointer value from the storage to which it points.

 

A pointer value contains a view consisting conceptually of:

 

a reference to shared backing storage
a current byte offset
the accessible length of the current view

 

The backing storage may represent:

 

a managed array
a Delphi dynamic array
owned native memory
borrowed native memory
a read-only C# string

 

Pointer, Pointer<T>, UntypedPointer, and PChar are value types. Assigning a pointer copies its current position, just as assigning a Delphi pointer copies an address:

 

Pointer<int> p = new Pointer<int>(values);

Pointer<int> q = p;

 

q++;

 

After this code, q points to the next element while p still points to the original element. Both pointers share the same backing storage, so writes performed through either pointer remain visible through the other pointer.

 

This distinction is important:

 

the pointer cursor has value semantics
the pointed-to storage has shared reference semantics

 

 

Null pointers

 

 

The default value of every pointer struct represents Delphi nil:

 

PChar p = default; 

Pointer raw = default; 

Pointer<int> typed = default;

 

 

Use IsNull() to test a pointer:

 

if (p.IsNull())

{

    // Delphi: if P = nil then

}

 

Use default or SetNull() to clear a pointer value:

 

p = default;

// or

p.SetNull();

 

Do not normally declare pointer structs as nullable values:

 

// Normally not recommended:

PChar? p = null;

 

default(PChar) already represents a null pointer.

 

Dereferencing a null pointer throws NullReferenceException. Access outside a known pointer view throws a bounds exception.

 

 

 

Shared views and pointer copies

 

Pointer conversions and pointer arithmetic normally create new views over the same backing storage. They do not copy the underlying data.

 

Pointer<int> typed = new Pointer<int>(values);

UntypedPointer bytes = typed.ToUntypedPointer();

Pointer raw = typed.ToPointer();

Pointer<int> next = typed + 1;

 

All these pointer values refer to the same storage.

 

Operations that explicitly create a value copy are documented separately. For example, UntypedPointer.FromValue<T>() creates a separate one-element backing and is not an alias to the original local variable.

 

 

Memory ownership

 

 

Pointer values do not individually own memory. Ownership belongs to the shared backing object.

 

This prevents a pointer copy from becoming a second owner of the same allocation.

 

Owned native memory is normally created by GetMem, AllocMem, Pointer(int), or UntypedPointer.AllocNative():

 

Pointer p = DelphiMemory.GetMem(256);

 

Release such memory with FreeMem:

 

DelphiMemory.FreeMem(ref p);

 

FreeMem releases the shared native backing and clears the supplied pointer variable. Every other pointer view that refers to the same backing becomes invalid.

 

SetNull() and Dispose() only clear the current pointer value. They do not release shared native memory:

 

Pointer q = p;

q.Dispose();       // Clears q only.

 

DelphiMemory.FreeMem(ref p); // Releases the allocation.

 

Borrowed native memory cannot be freed through the pointer runtime:

 

Pointer borrowed =

    new Pointer(address, size, ownsMemory: false);

 

Calling FreeMemory() or FreeMem for borrowed memory throws an exception.

 

 

Pointer arithmetic units

 

The unit used for arithmetic depends on the pointer type:

 

Pointer type

Arithmetic unit

Indexed access unit

UntypedPointer

byte

byte

Pointer

byte

byte

Pointer<byte>

byte

byte

Pointer<T>

element of T

element of T

PChar / PWideChar

UTF-16 code unit

UTF-16 code unit

 

 

Internally, all pointer views use byte offsets. Typed pointers convert element offsets to bytes by using DelphiTypeLayout.

 

 

Main pointer types

 

 

UntypedPointer

 

UntypedPointer is the canonical byte-addressed view used internally by the runtime. It is used for untyped Delphi parameters and for low-level operations such as Move and FillChar.

 

Pointer

 

Pointer corresponds to Delphi Pointer. It is a non-generic byte-addressed facade over UntypedPointer.

 

Pointer<T>

 

Pointer<T> represents a typed pointer. Dereferencing and pointer arithmetic use values of type T.

 

PByte

 

The current runtime normally represents Delphi PByte as Pointer<byte>. PByte does not require a separate storage implementation.

 

Depending on generator settings, generated code may use a type alias or the explicit type Pointer<byte>.

 

PChar and PWideChar

 

PChar represents a UTF-16 character pointer. In the current Unicode runtime, Delphi PWideChar uses the same representation.

 

 

Move and FillChar

 

 

Delphi Move and FillChar always use byte counts, even when the source or destination contains characters or records.

 

DelphiMemory.Move(

    source,

    destination,

    byteCount);

 

The runtime Move implementation:

 

copies bytes
supports overlapping source and destination regions
accepts odd byte counts for UTF-16 data
does not resize the destination
performs bounds checks when the backing length is known

 

The caller must allocate a sufficiently large destination before calling Move.

 

Native addresses and pinning

 

 

A pointer may be passed to a native API as an IntPtr:

 

IntPtr address = pointer.ToIntPtr();

 

For managed array or string backing, obtaining a persistent address pins the managed object. For scoped native calls, Pin() is preferred:

 

using PointerPin pin = pointer.Pin();

NativeFunction(pin.Address);

 

PointerPin.Dispose() releases the temporary pin.

 

If a native API returns an address inside an existing owned block, use an owner-relative view where available:

 

Pointer<TRecord> record =

    owner.CreateView<TRecord>(

        address,

        byteLength);

 

This preserves the relationship between the returned pointer and the owner backing. The owner must remain valid while the returned view is used.

 

 

Bounds and unsupported raw layouts

 

 

The runtime checks pointer ranges whenever the backing length is known. A pointer may be positioned one element past the end, but it cannot be dereferenced there.

 

A native pointer constructed with a zero capacity has an unknown length:

 

Pointer p = new Pointer(address);

 

In that case, complete bounds checking is not possible.

 

Raw byte access is supported for primitive values and records with a defined Delphi-compatible binary layout. Arrays whose elements contain managed references cannot be treated as arbitrary bytes.

 

Generated records used through raw pointers should normally have an explicit layout:

 

[StructLayout(LayoutKind.Sequential)]

public struct TMyRecord

{

    public int Value;

    public ushort Flags;

}

 

For packed or otherwise non-standard Delphi layouts, register the expected size and optionally a binary codec with DelphiTypeLayout.

 

 



This page belongs to the Delphi2C# Documentation

Delphi2C# home  Content