Skip to content

Refactor: Span stackalloc#7514

Open
JoonghyunCho wants to merge 2 commits intoSamsung:mainfrom
JoonghyunCho:refactor-span
Open

Refactor: Span stackalloc#7514
JoonghyunCho wants to merge 2 commits intoSamsung:mainfrom
JoonghyunCho:refactor-span

Conversation

@JoonghyunCho
Copy link
Member

@JoonghyunCho JoonghyunCho commented Mar 8, 2026

Replace Marshal.AllocHGlobal with stackalloc/fixed for low-risk interop patterns

Description

As part of .NET 8 refactoring, this PR replaces Marshal.AllocHGlobal + Marshal.StructureToPtr + Marshal.FreeHGlobal interop marshaling patterns with stackalloc / fixed to eliminate heap allocations.

We classified ~229 Marshal usage sites across TizenFX into 5 risk levels and applied changes only to the safest (Level 5) patterns.

Why was Marshal.AllocHGlobal used in the first place?

.NET's GC (Garbage Collector) may relocate managed memory during heap compaction. If a managed array's address is passed directly to a native C function, the GC could move that array while the native function is still using it, causing a dangling pointer crash. Marshal.AllocHGlobal allocates on the unmanaged heap, which the GC never touches — making it safe to pass to native code.

Downsides: heap allocation overhead + data copy cost + manual free required (missing free = memory leak).

Why are unsafe blocks needed?

Both stackalloc and fixed use C# pointer operations, which require an unsafe context.

Technique How it works GC Safety Cleanup
stackalloc Allocates on the stack (outside GC scope) ✅ Not a GC target Auto (scope exit)
fixed Pins managed array so GC cannot move it ✅ Pinned in place Auto (fixed exit)

Both eliminate copy + allocation cost compared to AllocHGlobal and cannot leak memory (no manual Free needed).


Changes

Tizen.Security.SecureRepository

SafeCipherParametersHandle.SetBuffer

  • AllocHGlobal + StructureToPtr + FreeHGlobalstackalloc (single blittable struct)
  • CkmcRawBuffer contains only IntPtr + UIntPtr fields — confirmed blittable
  • Synchronous call (ParamListSetBuffer) returns immediately → matches stack lifetime
-internal void SetBuffer(CipherParameterName name, byte[] value)
+internal unsafe void SetBuffer(CipherParameterName name, byte[] value)
 {
     var rawBuff = new Interop.CkmcRawBuffer(new PinnedObject(value), value.Length);
-    IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(rawBuff));
-    try {
-        Marshal.StructureToPtr<Interop.CkmcRawBuffer>(rawBuff, ptr, false);
-        Interop.CheckNThrowException(
-            Interop.CkmcTypes.ParamListSetBuffer(Ptr, (int)name, ptr), ...);
-    } finally {
-        Marshal.FreeHGlobal(ptr);
-    }
+    Interop.CkmcRawBuffer* ptr = stackalloc Interop.CkmcRawBuffer[1];
+    *ptr = rawBuff;
+    Interop.CheckNThrowException(
+        Interop.CkmcTypes.ParamListSetBuffer(Ptr, (int)name, (IntPtr)ptr), ...);
 }

Tizen.Location

PolygonBoundary constructor

  • AllocHGlobal + StructureToPtr loop → fixed pinning for direct array pass
  • Coordinate is [StructLayout(LayoutKind.Sequential)] with double × 2 — confirmed blittable
  • 🐛 Bug fix: Original code was missing FreeHGlobal call → memory leak fixed
 BoundaryType = BoundaryType.Polygon;
-IntPtr listPointer = Marshal.AllocHGlobal(Marshal.SizeOf(coordinates[0]) * coordinates.Count);
-for (int i = 0; i < coordinates.Count; i++)
-    Marshal.StructureToPtr(coordinates[i], listPointer + i * Marshal.SizeOf(coordinates[0]), false);
-int ret = Interop.LocationBoundary.CreatePolygonBoundary(listPointer, coordinates.Count, out boundsHandle);
+var coordArray = new Coordinate[coordinates.Count];
+for (int i = 0; i < coordinates.Count; i++)
+    coordArray[i] = coordinates[i];
+unsafe {
+    fixed (Coordinate* listPointer = coordArray) {
+        int ret = Interop.LocationBoundary.CreatePolygonBoundary(
+            (IntPtr)listPointer, coordinates.Count, out boundsHandle);
+    }
+}

csproj

  • Tizen.Security.SecureRepository.csproj: Added <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  • Tizen.Location.csproj: Added <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

Risk Analysis & Selection Criteria

Out of ~229 total Marshal usage sites, only Level 5 (Low risk) patterns were applied:

Risk Level Pattern Applied Reason
🔴 Critical IntPtr stored in field (TEEC, MetadataExtractor) Dangling pointer
🔴 Critical Native retains pointer asynchronously (VertexBuffer) Use-after-free
🟠 High Variable-size large data (video frames) StackOverflow
🟡 Medium Complex struct serialization (32/64-bit branching) Layout mismatch
🟢 Low Short-lived + fixed size + synchronous call This PR

GC Relocation Verification

Change Technique GC Safe Reason
SafeCipherParameters stackalloc Stack memory — outside GC scope
LocationBoundary fixed GC pinning — prevents relocation

Benchmark Results (on-device, Raspberry Pi 4)

Tested with 100,000 iterations per benchmark on a Tizen rpi4 target device.

Single Struct — CkmcRawBuffer pattern

Method Time Ops/sec Speedup
AllocHGlobal + StructureToPtr + FreeHGlobal 94 ms ~1.06M
stackalloc 3 ms ~33.3M ~31×

96.8% faster. AllocHGlobal issues an OS heap allocation (kernel call) → StructureToPtr copies memory → FreeHGlobal releases back to OS — all per invocation. stackalloc simply adjusts the stack pointer, achieving near-zero cost.

Struct Array[50] — Coordinate[] pattern

Method Time Ops/sec Speedup
AllocHGlobal + StructureToPtr × 50 + FreeHGlobal 1,039 ms ~96K
fixed pinning 2 ms ~50M ~520×

99.8% faster. The old approach marshals each struct individually (50× StructureToPtr calls per iteration). fixed pins the existing managed array in place — zero copy — and passes its address directly to native code.


Notes for Reviewers

  • No API signature changes — only internal method implementations modified
  • ElmSharp had a similar eligible pattern but was excluded (deprecated)
  • LocationBoundary's existing memory leak (FreeHGlobal missing) is naturally fixed by this change

@github-actions github-actions bot added the API14 Platform : Tizen 11.0 / TFM: net8.0-tizen11.0 label Mar 8, 2026
@TizenAPI-Bot
Copy link
Collaborator

Internal API Changed

Added: 8, Removed: 0, Changed: 0

Added

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates Tizen.NUI.BaseComponents.AnimatedImageView::AnimationState()

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ System.Void Tizen.NUI.BaseComponents.AnimatedImageView::Pause()

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ System.Void Tizen.NUI.BaseComponents.AnimatedImageView::Play()

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ System.Void Tizen.NUI.BaseComponents.AnimatedImageView::Stop()

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ static Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates::Paused

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ static Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates::Playing

+ /// <since_tizen>none</since_tizen
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ static Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates Tizen.NUI.BaseComponents.AnimatedImageView/AnimationStates::Stopped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API14 Platform : Tizen 11.0 / TFM: net8.0-tizen11.0 Internal API Changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants