Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C# Development #3602

Merged
merged 10 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
426 changes: 426 additions & 0 deletions wrappers/csharp/Documentation/cookbook.md

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions wrappers/csharp/Documentation/pinvoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Platform Invoke

This document contains notes on interoperability with the native SDK.

The .NET wrapper uses Platform Invoke (P/Invoke) to call functions from the unmanaged `realsense2` library.
It's a bit more tedious and error-prone than C++/CLI, but has better support across .NET implementations (.NET Framework, Core, Mono & Xamarin)

This means that the .NET wrapper exposes an object oriented API by calling into C functions, which is similar to what the C++ API does, with all the benefits and shortcomings of running in a managed environment, some of which discussed here.

## Lifetime & Resources

Just as the standard `System.IO.FileStream` is a wrapper around native OS file handles, most objects in the library are wrappers around their native counterparts.

Unmanaged resources are handled by the .NET [IDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.idisposable.dispose) interface, which provides a predictable way to release resources.
You should either directly call `Dispose` as soon as you're finished using the instance, or indirectly with the [using](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement) statement.

This is especially important for the `Frame` class and its derivatives, as the RealSense™ SDK uses a native frame pool, failing to release frames will cause the queue to fill, and no new frames will arrive.

> You cannot count on the GC to release these objects, it's unpredictable, it will not keep up, and isn't even guaranteed to run.

Trying to use a disposed object for a native call should raise an `ObjectDisposedException`.

## Memory & GC

Garbage collection means a *stop-the-world* event pauses all running threads while looking for unreferenced objects. This will lead to extreme spikes in frame times and can cause the RealSense™ device to drop frames.

We use an object pool to avoid allocating memory, and relieve GC pressure. Objects are rented from the pool, reinitialized to wrap a new unmanaged resource, and are released back to the pool when disposed.

Wrapped objects are finalizable, and will free their unmanaged resources before being collected, this should avoid native resource leaks in most but catastrophic failures.

## NativeMethods & Pointers

The `NativeMethods` class holds the extern function definitions (`rs2_*`) calling into the native `realsense2` library.

There are two types of `IntPtr` pointers used in these calls:

1. **Unmanaged** pointers coming from the native SDK itself

They are usually wrapped and operated on by the exposed API, or passed around in the internals of library.

These pointers are invisible to the GC, they won't be relocated, so they are safe to use in pinvoke calls, and should be freed when required by the SDK.

For instance:
* The pointer returned from `NativeMethods.rs2_pipeline_wait_for_frames` which is wrapped in a `Frame` object, and must be released with `NativeMethods.rs2_release_frame`

* `Frame.Data` returns a pointer to the start of the frame data, it comes from a call to `NativeMethods.rs2_get_frame_data` with the above pointer.
It's lifetime is bound to the frame and will be released along with it.

2. Pointers to **managed** objects

These can be relocated or freed by the GC (even while being used in native code) and special care is taken to ensure their safe use in unmanaged calls.

Some examples:
* In `Pipeline.Start(FrameCallback cb)` the `cb` delegate has to be kept alive since it will be called from unmanaged code.

* In `VideoFrame.CopyTo<T>(T[] array)` the array has to pinned so the GC won't move it while we're copying frame data.
6 changes: 6 additions & 0 deletions wrappers/csharp/Intel.RealSense/Base/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target_sources(${LRS_DOTNET_TARGET}
PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/DeleterHandle.cs"
"${CMAKE_CURRENT_LIST_DIR}/Object.cs"
"${CMAKE_CURRENT_LIST_DIR}/PooledObject.cs"
)
80 changes: 80 additions & 0 deletions wrappers/csharp/Intel.RealSense/Base/DeleterHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2017 Intel Corporation. All Rights Reserved.

namespace Intel.RealSense.Base
{
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;

[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void Deleter(IntPtr ptr);

/// <summary>
/// Native handle with deleter delegate to release unmanaged resources
/// </summary>
// TODO: CriticalFinalizerObject & CER
//[DebuggerDisplay("{deleter?.Method.Name,nq}", Name = nameof(Deleter))]
internal sealed class DeleterHandle : IDisposable
{
private IntPtr handle;
private Deleter deleter;

public IntPtr Handle => handle;

/// <summary>
/// Gets a value indicating whether this handle is invalid
/// </summary>
public bool IsInvalid => handle == IntPtr.Zero;

public DeleterHandle(IntPtr ptr, Deleter deleter)
{
handle = ptr;
this.deleter = deleter;
}

public void SetHandleAsInvalid()
{
handle = IntPtr.Zero;
GC.SuppressFinalize(this);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

public void Dispose(bool disposing)
{
if (handle == IntPtr.Zero)
{
return;
}

deleter?.Invoke(handle);
handle = IntPtr.Zero;
}

internal void Reset(IntPtr ptr)
{
handle = ptr;
GC.ReRegisterForFinalize(this);
}

internal void Reset(IntPtr ptr, Deleter deleter)
{
this.handle = ptr;
this.deleter = deleter;
//GC.ReRegisterForFinalize(this);
}

~DeleterHandle()
{
//Console.WriteLine($"~{handle} {deleter?.Method}");
Dispose(false);
}
}
}
67 changes: 67 additions & 0 deletions wrappers/csharp/Intel.RealSense/Base/Object.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2017 Intel Corporation. All Rights Reserved.

namespace Intel.RealSense.Base
{
using System;
using System.Diagnostics;

/// <summary>
/// Base class for disposable objects with native resources
/// </summary>
public abstract class Object : IDisposable
{
// TODO: rename, kept for backwards compatiblity
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal readonly DeleterHandle m_instance;

/// <summary>
/// Initializes a new instance of the <see cref="Object"/> class.
/// </summary>
/// <param name="ptr">native pointer</param>
/// <param name="deleter">optional deleter</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="ptr"/> is null</exception>
protected Object(IntPtr ptr, Deleter deleter)
{
if (ptr == IntPtr.Zero)
{
throw new ArgumentNullException(nameof(ptr));
}

m_instance = new DeleterHandle(ptr, deleter);
}

/// <summary>
/// Gets the native handle
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown when <see cref="DeleterHandle.IsInvalid"/></exception>
public IntPtr Handle
{
get
{
if (m_instance.IsInvalid)
{
throw new ObjectDisposedException(GetType().Name);
}

return m_instance.Handle;
}
}

/// <inheritdoc/>
public void Dispose()
{
this.Dispose(true);
}

protected virtual void Dispose(bool disposing)
{
m_instance.Dispose();
}

internal void Reset(IntPtr ptr, Deleter deleter)
{
m_instance.Reset(ptr, deleter);
}
}
}
31 changes: 31 additions & 0 deletions wrappers/csharp/Intel.RealSense/Base/PooledObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// License: Apache 2.0. See LICENSE file in root directory.
// Copyright(c) 2017 Intel Corporation. All Rights Reserved.

namespace Intel.RealSense.Base
{
using System;

/// <summary>
/// Base class for objects in an <cref see="ObjectPool">ObjectPool</cref>
/// </summary>
public abstract class PooledObject : Object
{
protected PooledObject(IntPtr ptr, Deleter deleter)
: base(ptr, deleter)
{
}

internal abstract void Initialize();

protected override void Dispose(bool disposing)
{
if (m_instance.IsInvalid)
{
return;
}

base.Dispose(disposing);
ObjectPool.Release(this);
}
}
}
8 changes: 7 additions & 1 deletion wrappers/csharp/Intel.RealSense/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ add_library(${PROJECT_NAME}
Context.cs
NativeMethods.cs
.nuget/Intel.RealSense.targets

GlobalSuppressions.cs
Properties/AssemblyInfo.cs
packages.config
stylecop.json
)

include(Base/CMakeLists.txt)
include(Devices/CMakeLists.txt)
include(Frames/CMakeLists.txt)
include(Pipeline/CMakeLists.txt)
Expand All @@ -20,6 +23,7 @@ include(StreamProfiles/CMakeLists.txt)
include(Sensors/CMakeLists.txt)
include(Types/CMakeLists.txt)
include(Helpers/CMakeLists.txt)
include(Options/CMakeLists.txt)

csharp_set_designer_cs_properties(
.nuget/Intel.RealSense.targets
Expand Down Expand Up @@ -70,6 +74,8 @@ set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DOTNET_REFERENCES

target_compile_options(${PROJECT_NAME} PRIVATE /define:${CMAKE_SYSTEM_NAME})

set_source_files_properties(stylecop.json PROPERTIES VS_TOOL_OVERRIDE "AdditionalFiles")

install(TARGETS ${PROJECT_NAME}
EXPORT realsense2Targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
Expand Down
Loading