Skip to content

Commit

Permalink
[bgen] Add support for [return: Release] for types subclassing Native…
Browse files Browse the repository at this point in the history
…Object or implementing INativeObject. (#21752)

Add support for [return: Release] for types subclassing NativeObject or implementing INativeObject.

This was implemented by always passing a value for the "owns" parameter when
creating a managed wrapper for a given native pointer (and the passed in value
for the "owns" parameter honors the presence of the Release attribute)

This also required adding an overload taking an "owns" parameter for a few
types which didn't already have such an overload.
  • Loading branch information
rolfbjarne authored Dec 6, 2024
1 parent 90b387f commit a8e360d
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 52 deletions.
7 changes: 6 additions & 1 deletion src/AddressBook/ABRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,15 @@ internal ABRecord (NativeHandle handle, bool owns)
}

public static ABRecord? FromHandle (IntPtr handle)
{
return FromHandle (handle, false);
}

internal static ABRecord? FromHandle (IntPtr handle, bool owns)
{
if (handle == IntPtr.Zero)
return null;
return FromHandle (handle, null, false);
return FromHandle (handle, null, owns);
}

internal static ABRecord FromHandle (IntPtr handle, ABAddressBook? addressbook, bool owns = true)
Expand Down
7 changes: 6 additions & 1 deletion src/AudioToolbox/MusicSequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ protected override void Dispose (bool disposing)
static readonly Dictionary<IntPtr, WeakReference> sequenceMap = new Dictionary<IntPtr, WeakReference> (Runtime.IntPtrEqualityComparer);

internal static MusicSequence Lookup (IntPtr handle)
{
return Lookup (handle, false);
}

internal static MusicSequence Lookup (IntPtr handle, bool owns)
{
lock (sequenceMap) {
if (sequenceMap.TryGetValue (handle, out var weakRef)) {
Expand All @@ -120,7 +125,7 @@ internal static MusicSequence Lookup (IntPtr handle)
}
sequenceMap.Remove (handle);
}
var ms = new MusicSequence (handle, false);
var ms = new MusicSequence (handle, owns);
sequenceMap [handle] = new WeakReference (ms);
return ms;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Foundation/NSArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,18 @@ static public T [] ArrayFromHandleFunc<T> (NativeHandle handle, Func<NativeHandl
return ret;
}

/// <summary>Create a managed array from a pointer to a native NSArray instance.</summary>
/// <param name="handle">The pointer to the native NSArray instance.</param>
/// <param name="createObject">A callback that returns an instance of the type T for a given pointer (for an element in the NSArray).</param>
/// <param name="releaseHandle">Whether the native NSArray instance should be released before returning or not.</param>
public static T [] ArrayFromHandleFunc<T> (NativeHandle handle, Func<NativeHandle, T> createObject, bool releaseHandle)
{
var rv = ArrayFromHandleFunc<T> (handle, createObject);
if (releaseHandle && handle != NativeHandle.Zero)
NSObject.DangerousRelease (handle);
return rv;
}

static public T [] ArrayFromHandle<T> (NativeHandle handle, Converter<NativeHandle, T> creator)
{
if (handle == NativeHandle.Zero)
Expand Down
2 changes: 1 addition & 1 deletion src/MediaToolbox/MTAudioProcessingTap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ delegate void MTAudioProcessingTapProcessCallbackProxy (/* MTAudioProcessingTapR

MTAudioProcessingTapCallbacks callbacks;

internal static MTAudioProcessingTap? FromHandle (IntPtr handle)
internal static MTAudioProcessingTap? FromHandle (IntPtr handle, bool owns)
{
lock (handles){
if (handles.TryGetValue (handle, out var ret))
Expand Down
27 changes: 25 additions & 2 deletions src/ObjCRuntime/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,18 @@ static IntPtr CreateNSObject (IntPtr type_gchandle, IntPtr handle, NSObject.Flag
{
return GetNSObject ((IntPtr) ptr, MissingCtorResolution.ThrowConstructor1NotFound);
}

/// <summary>Wraps an unmanaged <see cref="NativeHandle" /> into a fully typed <see cref="NSObject" />, or returns an existing wrapper object if one already exists.</summary>
/// <param name="ptr">A pointer to an unmanaged <see cref="NSObject" /> or any class that derives from the Objective-C NSObject class.</param>
/// <param name="owns">Pass true if the caller has a reference to the native object, and wants to give it to the managed wrapper instance. Otherwise pass false (and the native object will be retained if needed).</param>
/// <returns>An instance of a class that derives <see cref="NSObject" />.</returns>
/// <remarks>
/// <para>The runtime create an instance of the most derived managed class.</para>
/// </remarks>
public static NSObject? GetNSObject (NativeHandle ptr, bool owns)
{
return GetNSObject ((IntPtr) ptr, owns, MissingCtorResolution.ThrowConstructor1NotFound);
}
#endif

public static NSObject? GetNSObject (IntPtr ptr)
Expand All @@ -1838,16 +1850,27 @@ static IntPtr CreateNSObject (IntPtr type_gchandle, IntPtr handle, NSObject.Flag
}

internal static NSObject? GetNSObject (IntPtr ptr, MissingCtorResolution missingCtorResolution, bool evenInFinalizerQueue = false)
{
return GetNSObject (ptr, false, missingCtorResolution, evenInFinalizerQueue);
}

internal static NSObject? GetNSObject (IntPtr ptr, bool owns, MissingCtorResolution missingCtorResolution, bool evenInFinalizerQueue = false)
{
if (ptr == IntPtr.Zero)
return null;

var o = TryGetNSObject (ptr, evenInFinalizerQueue);

if (o is not null)
if (o is not null) {
if (owns)
o.DangerousRelease ();
return o;
}

return ConstructNSObject (ptr, Class.GetClassForObject (ptr), missingCtorResolution);
o = ConstructNSObject (ptr, Class.GetClassForObject (ptr), missingCtorResolution);
if (owns)
NSObject.DangerousRelease (ptr);
return o;
}

static public T? GetNSObject<T> (IntPtr ptr) where T : NSObject
Expand Down
9 changes: 9 additions & 0 deletions src/ObjCRuntime/Selector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ internal static string GetName (IntPtr handle)
return new Selector (sel, false);
}

/// <summary>Creates a managed Selector instance from a native selector.</summary>
/// <param name="selector">The native selector handle.</param>
/// <param name="owns">Whether the caller owns the native selector handle or not.</param>
/// <remarks>It's not possible to free a selector, so the <paramref name="owns" /> parameter is ignored.</remarks>
public static Selector? FromHandle (NativeHandle selector, bool owns)
{
return FromHandle (selector);
}

public static Selector Register (NativeHandle handle)
{
return new Selector (handle);
Expand Down
1 change: 1 addition & 0 deletions src/bgen/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public RetainAttribute (string wrap)
public string WrapName { get; set; }
}

[AttributeUsage (AttributeTargets.ReturnValue, AllowMultiple = false)]
public class ReleaseAttribute : Attribute {
}

Expand Down
2 changes: 2 additions & 0 deletions src/bgen/Caches/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class TypeCache {
/* fundamental */
public Type NSObject { get; }
public Type INativeObject { get; }
public Type NativeObject { get; }

/* objcruntime */
public Type BlockLiteral { get; }
Expand Down Expand Up @@ -195,6 +196,7 @@ public TypeCache (MetadataLoadContext universe, Frameworks frameworks, PlatformN
/* fundamental */
NSObject = Lookup (platformAssembly, "Foundation", "NSObject");
INativeObject = Lookup (platformAssembly, "ObjCRuntime", "INativeObject");
NativeObject = Lookup (platformAssembly, "CoreFoundation", "NativeObject");

/* objcruntime */
BlockLiteral = Lookup (platformAssembly, "ObjCRuntime", "BlockLiteral");
Expand Down
58 changes: 22 additions & 36 deletions src/bgen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@

// Disable until we get around to enable + fix any issues.
#nullable disable
// but allow annotation source code with nullability info.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

public partial class Generator : IMemberGatherer {
internal bool IsPublicMode;
Expand Down Expand Up @@ -536,7 +538,7 @@ string GetFromBindAsWrapper (MemberInformation minfo, out string suffix)
return append;
}

public bool HasForcedAttribute (ICustomAttributeProvider cu, out string owns)
public bool HasForcedAttribute (ICustomAttributeProvider cu, out bool owns)
{
var att = AttributeManager.GetCustomAttribute<ForcedTypeAttribute> (cu);

Expand All @@ -547,11 +549,11 @@ public bool HasForcedAttribute (ICustomAttributeProvider cu, out string owns)
}

if (att is null) {
owns = "false";
owns = false;
return false;
}

owns = att.Owns ? "true" : "false";
owns = att.Owns;
return true;
}

Expand Down Expand Up @@ -606,8 +608,7 @@ public TrampolineInfo MakeTrampoline (Type t)
pars.Add (new TrampolineParameterInfo ("IntPtr", "block"));
var parameters = mi.GetParameters ();
foreach (var pi in parameters) {
string isForcedOwns;
var isForced = HasForcedAttribute (pi, out isForcedOwns);
var isForced = HasForcedAttribute (pi, out var isForcedOwns);

if (pi != parameters [0])
invoke.Append (", ");
Expand All @@ -619,7 +620,7 @@ public TrampolineInfo MakeTrampoline (Type t)
if (IsProtocolInterface (pi.ParameterType)) {
invoke.AppendFormat (" Runtime.GetINativeObject<{1}> ({0}, false)!", safe_name, pi.ParameterType);
} else if (isForced) {
invoke.AppendFormat (" Runtime.GetINativeObject<{1}> ({0}, true, {2})!", safe_name, TypeManager.RenderType (pi.ParameterType), isForcedOwns);
invoke.AppendFormat (" Runtime.GetINativeObject<{1}> ({0}, true, {2})!", safe_name, TypeManager.RenderType (pi.ParameterType), isForcedOwns ? "true" : "false");
} else if (IsNSObject (pi.ParameterType)) {
invoke.AppendFormat (" Runtime.GetNSObject<{1}> ({0})!", safe_name, TypeManager.RenderType (pi.ParameterType));
} else {
Expand Down Expand Up @@ -2886,22 +2887,23 @@ void GetReturnsWrappers (MethodInfo mi, MemberInformation minfo, Type declaringT
MarshalInfo mai = new MarshalInfo (this, mi);
MarshalType mt;

var owns = (minfo?.is_return_release == true) || (minfo?.is_forced_owns == true) ? "true" : "false";
if (GetNativeEnumToManagedExpression (mi.ReturnType, out cast_a, out cast_b, out var _, postproc)) {
// we're done here
} else if (mi.ReturnType.IsEnum) {
cast_a = "(" + TypeManager.FormatType (minfo?.type ?? mi.DeclaringType, mi.ReturnType) + ") ";
cast_b = "";
} else if (marshalTypes.TryGetMarshalType (mai.Type, out mt)) {
cast_a = mt.CreateFromRet;
cast_b = mt.ClosingCreate;
cast_b = mt.ClosingCreate?.Replace ("%OWNS%", owns) ?? string.Empty;
} else if (TypeManager.IsWrappedType (mi.ReturnType)) {
// protocol support means we can return interfaces and, as far as .NET knows, they might not be NSObject
if (IsProtocolInterface (mi.ReturnType)) {
cast_a = " Runtime.GetINativeObject<" + TypeManager.FormatType (minfo?.type ?? mi.DeclaringType, mi.ReturnType) + "> (";
cast_b = $", {(minfo?.is_return_release == true ? "true" : "false")})!";
cast_b = $", {owns})!";
} else if (minfo is not null && minfo.is_forced) {
cast_a = " Runtime.GetINativeObject<" + TypeManager.FormatType (minfo.type, mi.ReturnType) + "> (";
cast_b = $", true, {minfo.is_forced_owns})!";
cast_b = $", true, {owns})!";
} else if (minfo is not null && minfo.is_bindAs) {
var bindAs = GetBindAsAttribute (minfo.mi);
var nullableBindAsType = TypeManager.GetUnderlyingNullableType (bindAs.Type);
Expand All @@ -2915,27 +2917,27 @@ void GetReturnsWrappers (MethodInfo mi, MemberInformation minfo, Type declaringT
if (isNullable) {
print ("{0} retvaltmp;", NativeHandleType);
cast_a = "((retvaltmp = ";
cast_b = $") == IntPtr.Zero ? default ({formattedBindAsType}) : ({wrapper}Runtime.GetNSObject<{formattedReturnType}> (retvaltmp)!){suffix})";
cast_b = $") == IntPtr.Zero ? default ({formattedBindAsType}) : ({wrapper}Runtime.GetNSObject<{formattedReturnType}> (retvaltmp, {owns})!){suffix})";
} else {
cast_a = $"{wrapper}Runtime.GetNSObject<{formattedReturnType}> (";
cast_b = $")!{suffix}";
cast_b = $", {owns})!{suffix}";
}
} else {
var enumCast = (bindAsType.IsEnum && !minfo.type.IsArray) ? $"({formattedBindAsType}) " : string.Empty;
print ("{0} retvaltmp;", NativeHandleType);
cast_a = "((retvaltmp = ";
cast_b = $") == IntPtr.Zero ? default ({formattedBindAsType}) : ({enumCast}Runtime.GetNSObject<{formattedReturnType}> (retvaltmp)!{wrapper})){suffix}";
cast_b = $") == IntPtr.Zero ? default ({formattedBindAsType}) : ({enumCast}Runtime.GetNSObject<{formattedReturnType}> (retvaltmp, {owns})!{wrapper})){suffix}";
}
} else {
cast_a = " Runtime.GetNSObject<" + TypeManager.FormatType (minfo?.type ?? declaringType, mi.ReturnType) + "> (";
cast_b = ")!";
cast_b = $", {owns})!";
}
} else if (mi.ReturnType.IsGenericParameter) {
cast_a = " Runtime.GetINativeObject<" + mi.ReturnType.Name + "> (";
cast_b = ", false)!";
cast_b = $", {owns})!";
} else if (mai.Type == TypeCache.System_String && !mai.PlainString) {
cast_a = "CFString.FromHandle (";
cast_b = ")!";
cast_b = $", {owns})!";
} else if (mi.ReturnType.IsSubclassOf (TypeCache.System_Delegate)) {
cast_a = "";
cast_b = "";
Expand All @@ -2951,19 +2953,19 @@ void GetReturnsWrappers (MethodInfo mi, MemberInformation minfo, Type declaringT
print ("{0} retvalarrtmp;", NativeHandleType);
cast_a = "((retvalarrtmp = ";
cast_b = ") == IntPtr.Zero ? null! : (";
cast_b += $"NSArray.ArrayFromHandleFunc <{TypeManager.FormatType (bindAsT.DeclaringType, bindAsT)}> (retvalarrtmp, {GetFromBindAsWrapper (minfo, out suffix)})" + suffix;
cast_b += "))";
cast_b += $"NSArray.ArrayFromHandleFunc <{TypeManager.FormatType (bindAsT.DeclaringType, bindAsT)}> (retvalarrtmp, {GetFromBindAsWrapper (minfo, out suffix)}, {owns})" + suffix;
cast_b += $"))";
} else if (etype == TypeCache.System_String) {
cast_a = "CFArray.StringArrayFromHandle (";
cast_b = ")!";
cast_b = $", {owns})!";
} else if (etype == TypeCache.Selector) {
exceptions.Add (ErrorHelper.CreateError (1066, mai.Type.FullName, mi.DeclaringType.FullName, mi.Name));
} else {
if (NamespaceCache.NamespacesThatConflictWithTypes.Contains (etype.Namespace))
cast_a = "CFArray.ArrayFromHandle<global::" + etype + ">(";
else
cast_a = "CFArray.ArrayFromHandle<" + TypeManager.FormatType (mi.DeclaringType, etype) + ">(";
cast_b = ")!";
cast_b = $", {owns})!";
}
} else if (mi.ReturnType.Namespace == "System" && mi.ReturnType.Name == "nint") {
cast_a = "(nint) ";
Expand Down Expand Up @@ -3418,7 +3420,7 @@ void GenerateTypeLowering (MethodInfo mi, bool null_allowed_override, out String
} else if (isINativeObjectSubclass) {
if (!pi.IsOut)
by_ref_processing.AppendFormat ("if ({0}Value != ({0} is null ? NativeHandle.Zero : {0}.Handle))\n\t", pi.Name.GetSafeParamName ());
by_ref_processing.AppendFormat ("{0} = Runtime.GetINativeObject<{1}> ({0}Value, {2}, {3})!;\n", pi.Name.GetSafeParamName (), TypeManager.RenderType (elementType), isForcedType ? "true" : "false", isForcedType ? isForcedOwns : "false");
by_ref_processing.AppendFormat ("{0} = Runtime.GetINativeObject<{1}> ({0}Value, {2}, {3})!;\n", pi.Name.GetSafeParamName (), TypeManager.RenderType (elementType), isForcedType ? "true" : "false", (isForcedType && isForcedOwns) ? "true" : "false");
} else {
throw ErrorHelper.CreateError (88, mai.Type, mi);
}
Expand Down Expand Up @@ -3650,22 +3652,6 @@ public void GenerateMethodBody (MemberInformation minfo, MethodInfo mi, string s
if (shouldMarshalNativeExceptions)
print ("Runtime.ThrowException (exception_gchandle);");

if (minfo.is_return_release && !IsProtocolInterface (mi.ReturnType)) {

// Make sure we generate the required signature in Messaging only if needed
// bool_objc_msgSendSuper_IntPtr: for respondsToSelector:
if (!send_methods.ContainsKey ("void_objc_msgSend")) {
print (m, "[DllImport (LIBOBJC_DYLIB, EntryPoint=\"objc_msgSendSuper\")]");
print (m, "public extern static void void_objc_msgSend (IntPtr receiever, IntPtr selector);");
RegisterMethodName ("void_objc_msgSend");
}

print ("if (ret is not null)");
indent++;
print ("global::{0}.void_objc_msgSend (ret.Handle, Selector.GetHandle (\"release\"));", NamespaceCache.Messaging);
indent--;
}

Inject<PostSnippetAttribute> (mi);

if (disposes.Length > 0)
Expand Down
4 changes: 2 additions & 2 deletions src/bgen/Models/MarshalType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ public class MarshalType {
public string CreateFromRet { get; }
public string? ClosingCreate { get; }

public MarshalType (Type t, string? encode = null, string? fetch = null, string? create = null, string? closingCreate = ")")
public MarshalType (Type t, string? encode = null, string? fetch = null, string? create = null, string? closingCreate = ", %OWNS%)")
{
Type = t;
Encoding = encode ?? Generator.NativeHandleType;
ParameterMarshal = fetch ?? "{0}.Handle";
if (create is null) {
CreateFromRet = $"Runtime.GetINativeObject<global::{t.FullName}> (";
ClosingCreate = ", false)!";
ClosingCreate = ", %OWNS%)!";
} else {
CreateFromRet = create;
ClosingCreate = closingCreate;
Expand Down
16 changes: 8 additions & 8 deletions src/bgen/Models/MarshalTypeList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public class MarshalTypeList : List<MarshalType> {

public void Load (TypeCache typeCache, Frameworks frameworks)
{
Add (new MarshalType (typeCache.NSObject, create: "Runtime.GetNSObject (", closingCreate: ")!"));
Add (new MarshalType (typeCache.Selector, create: "Selector.FromHandle (", closingCreate: ")!"));
Add (new MarshalType (typeCache.NSObject, create: "Runtime.GetNSObject (", closingCreate: ", %OWNS%)!"));
Add (new MarshalType (typeCache.Selector, create: "Selector.FromHandle (", closingCreate: ", %OWNS%)!"));
Add (new MarshalType (typeCache.BlockLiteral, "BlockLiteral", "{0}", "THIS_IS_BROKEN"));
if (typeCache.MusicSequence is not null)
Add (new MarshalType (typeCache.MusicSequence, create: "global::AudioToolbox.MusicSequence.Lookup ("));
Expand Down Expand Up @@ -42,15 +42,15 @@ public void Load (TypeCache typeCache, Frameworks frameworks)
Add (typeCache.CVImageBuffer);
}
if (frameworks.HaveMediaToolbox)
Add (new MarshalType (typeCache.MTAudioProcessingTap!, create: "MediaToolbox.MTAudioProcessingTap.FromHandle("));
Add (new MarshalType (typeCache.MTAudioProcessingTap!, create: "MediaToolbox.MTAudioProcessingTap.FromHandle (", closingCreate: ", %OWNS%)!"));
if (frameworks.HaveAddressBook) {
Add (typeCache.ABAddressBook);
Add (new MarshalType (typeCache.ABPerson!, create: "(ABPerson) ABRecord.FromHandle (", closingCreate: ")!"));
Add (new MarshalType (typeCache.ABRecord!, create: "ABRecord.FromHandle (", closingCreate: ")!"));
Add (new MarshalType (typeCache.ABPerson!, create: "(ABPerson) ABRecord.FromHandle (", closingCreate: ", %OWNS%)!"));
Add (new MarshalType (typeCache.ABRecord!, create: "ABRecord.FromHandle (", closingCreate: ", %OWNS%)!"));
}
if (frameworks.HaveCoreVideo) {
// owns `false` like ptr ctor https://github.com/xamarin/xamarin-macios/blob/6f68ab6f79c5f1d96d2cbb1e697330623164e46d/src/CoreVideo/CVBuffer.cs#L74-L90
Add (new MarshalType (typeCache.CVPixelBuffer!, create: "Runtime.GetINativeObject<CVPixelBuffer> (", closingCreate: ", false)!"));
Add (new MarshalType (typeCache.CVPixelBuffer!, create: "Runtime.GetINativeObject<CVPixelBuffer> (", closingCreate: ", %OWNS%)!"));
}
Add (typeCache.CGLayer);
if (frameworks.HaveCoreMedia)
Expand All @@ -63,7 +63,7 @@ public void Load (TypeCache typeCache, Frameworks frameworks)
if (frameworks.HaveAudioUnit)
Add (typeCache.AudioComponent);
if (frameworks.HaveCoreMedia) {
Add (new MarshalType (typeCache.CMFormatDescription!, create: "CMFormatDescription.Create (", closingCreate: ")!"));
Add (new MarshalType (typeCache.CMFormatDescription!, create: "CMFormatDescription.Create (", closingCreate: ", %OWNS%)!"));
Add (typeCache.CMAudioFormatDescription);
Add (typeCache.CMVideoFormatDescription);
}
Expand All @@ -77,7 +77,7 @@ public void Load (TypeCache typeCache, Frameworks frameworks)
Add (typeCache.SecProtocolOptions);
Add (typeCache.SecProtocolMetadata);
Add (typeCache.SecAccessControl);
Add (new MarshalType (typeCache.AudioBuffers, create: "new global::AudioToolbox.AudioBuffers (", closingCreate: ", false)"));
Add (new MarshalType (typeCache.AudioBuffers, create: "new global::AudioToolbox.AudioBuffers (", closingCreate: ", %OWNS%)"));
if (frameworks.HaveAudioUnit) {
Add (typeCache.AURenderEventEnumerator);
}
Expand Down
2 changes: 1 addition & 1 deletion src/bgen/Models/MemberInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class MemberInformation {
// know whether the code is to call the internal static method implementing a
// protocol member (in which case this property is true). See also is_protocol_implementation_method.
public bool call_protocol_implementation_method;
public string is_forced_owns;
public bool is_forced_owns;
public bool is_bindAs => Generator.HasBindAsAttribute (mi);
public bool generate_is_async_overload;

Expand Down
Loading

10 comments on commit a8e360d

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

@vs-mobiletools-engineering-service2

This comment was marked as outdated.

Please sign in to comment.