mirror of https://github.com/mono/CppSharp.git
c-sharpdotnetmonobindingsbridgecclangcpluspluscppsharpglueinteropparserparsingpinvokeswigsyntax-treevisitorsxamarinxamarin-bindings
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
678 lines
23 KiB
678 lines
23 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Text; |
|
using CppSharp.AST; |
|
using CppSharp.AST.Extensions; |
|
using CppSharp.Types; |
|
using Type = CppSharp.AST.Type; |
|
|
|
namespace CppSharp.Generators.CSharp |
|
{ |
|
public enum CSharpMarshalKind |
|
{ |
|
Unknown, |
|
NativeField |
|
} |
|
|
|
public class CSharpMarshalContext : MarshalContext |
|
{ |
|
public CSharpMarshalContext(Driver driver) |
|
: base(driver) |
|
{ |
|
Kind = CSharpMarshalKind.Unknown; |
|
ArgumentPrefix = new TextGenerator(); |
|
Cleanup = new TextGenerator(); |
|
} |
|
|
|
public CSharpMarshalKind Kind { get; set; } |
|
public QualifiedType FullType; |
|
|
|
public TextGenerator ArgumentPrefix { get; private set; } |
|
public TextGenerator Cleanup { get; private set; } |
|
} |
|
|
|
public abstract class CSharpMarshalPrinter : MarshalPrinter |
|
{ |
|
public CSharpMarshalContext CSharpContext |
|
{ |
|
get { return Context as CSharpMarshalContext; } |
|
} |
|
|
|
protected CSharpMarshalPrinter(CSharpMarshalContext context) |
|
: base(context) |
|
{ |
|
Options.VisitFunctionParameters = false; |
|
Options.VisitTemplateArguments = false; |
|
} |
|
|
|
public override bool VisitMemberPointerType(MemberPointerType member, |
|
TypeQualifiers quals) |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
public class CSharpMarshalNativeToManagedPrinter : CSharpMarshalPrinter |
|
{ |
|
public CSharpMarshalNativeToManagedPrinter(CSharpMarshalContext context) |
|
: base(context) |
|
{ |
|
Context.MarshalToManaged = this; |
|
} |
|
|
|
public int VarSuffix { get; set; } |
|
|
|
public static string QualifiedIdentifier(Declaration decl) |
|
{ |
|
var names = new List<string> { decl.Name }; |
|
|
|
var ctx = decl.Namespace; |
|
while (ctx != null) |
|
{ |
|
if (!string.IsNullOrWhiteSpace(ctx.Name)) |
|
names.Add(ctx.Name); |
|
ctx = ctx.Namespace; |
|
} |
|
|
|
//if (Options.GenerateLibraryNamespace) |
|
// names.Add(Options.OutputNamespace); |
|
|
|
names.Reverse(); |
|
return string.Join(".", names); |
|
} |
|
|
|
public override bool VisitType(Type type, TypeQualifiers quals) |
|
{ |
|
TypeMap typeMap; |
|
if (Context.Driver.TypeDatabase.FindTypeMap(type, out typeMap) && typeMap.DoesMarshalling) |
|
{ |
|
typeMap.Type = type; |
|
typeMap.CSharpMarshalToManaged(Context); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitDeclaration(Declaration decl) |
|
{ |
|
TypeMap typeMap; |
|
if (Context.Driver.TypeDatabase.FindTypeMap(decl, out typeMap) && typeMap.DoesMarshalling) |
|
{ |
|
typeMap.Declaration = decl; |
|
typeMap.CSharpMarshalToManaged(Context); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitArrayType(ArrayType array, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(array, quals)) |
|
return false; |
|
|
|
switch (array.SizeType) |
|
{ |
|
case ArrayType.ArraySize.Constant: |
|
var supportBefore = Context.SupportBefore; |
|
string value = Generator.GeneratedIdentifier("value"); |
|
supportBefore.WriteLine("{0}[] {1} = null;", array.Type, value, array.Size); |
|
supportBefore.WriteLine("if ({0} != null)", Context.ReturnVarName); |
|
supportBefore.WriteStartBraceIndent(); |
|
supportBefore.WriteLine("{0} = new {1}[{2}];", value, array.Type, array.Size); |
|
supportBefore.WriteLine("for (int i = 0; i < {0}; i++)", array.Size); |
|
supportBefore.WriteLineIndent("{0}[i] = {1}[i];", value, Context.ReturnVarName); |
|
supportBefore.WriteCloseBraceIndent(); |
|
Context.Return.Write(value); |
|
break; |
|
case ArrayType.ArraySize.Variable: |
|
Context.Return.Write("null"); |
|
break; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(pointer, quals)) |
|
return false; |
|
|
|
var pointee = pointer.Pointee.Desugar(); |
|
|
|
if (CSharpTypePrinter.IsConstCharString(pointer)) |
|
{ |
|
Context.Return.Write(MarshalStringToManaged(Context.ReturnVarName)); |
|
return true; |
|
} |
|
|
|
PrimitiveType primitive; |
|
if (pointee.IsPrimitiveType(out primitive)) |
|
{ |
|
var param = Context.Parameter; |
|
if (param != null && (param.IsOut || param.IsInOut)) |
|
{ |
|
Context.Return.Write("_{0}", param.Name); |
|
return true; |
|
} |
|
|
|
Context.Return.Write(Context.ReturnVarName); |
|
return true; |
|
} |
|
|
|
return pointer.Pointee.Visit(this, quals); |
|
} |
|
|
|
private string MarshalStringToManaged(string varName) |
|
{ |
|
if (Equals(Context.Driver.Options.Encoding, Encoding.ASCII)) |
|
{ |
|
return string.Format("Marshal.PtrToStringAnsi({0})", varName); |
|
} |
|
if (Equals(Context.Driver.Options.Encoding, Encoding.Unicode) || |
|
Equals(Context.Driver.Options.Encoding, Encoding.BigEndianUnicode)) |
|
{ |
|
return string.Format("Marshal.PtrToStringUni({0})", varName); |
|
} |
|
throw new NotSupportedException(string.Format("{0} is not supported yet.", |
|
Context.Driver.Options.Encoding.EncodingName)); |
|
} |
|
|
|
public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers quals) |
|
{ |
|
switch (primitive) |
|
{ |
|
case PrimitiveType.Void: |
|
return true; |
|
case PrimitiveType.Bool: |
|
case PrimitiveType.Int8: |
|
case PrimitiveType.UInt8: |
|
case PrimitiveType.Int16: |
|
case PrimitiveType.UInt16: |
|
case PrimitiveType.Int32: |
|
case PrimitiveType.UInt32: |
|
case PrimitiveType.Int64: |
|
case PrimitiveType.UInt64: |
|
case PrimitiveType.Float: |
|
case PrimitiveType.Double: |
|
Context.Return.Write(Context.ReturnVarName); |
|
return true; |
|
case PrimitiveType.Char16: |
|
case PrimitiveType.WideChar: |
|
return false; |
|
} |
|
|
|
throw new NotImplementedException(); |
|
} |
|
|
|
public override bool VisitTypedefType(TypedefType typedef, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(typedef, quals)) |
|
return false; |
|
|
|
var decl = typedef.Declaration; |
|
|
|
FunctionType function; |
|
if (decl.Type.IsPointerTo(out function)) |
|
{ |
|
var ptrName = Generator.GeneratedIdentifier("ptr") + |
|
Context.ParameterIndex; |
|
|
|
Context.SupportBefore.WriteLine("var {0} = {1};", ptrName, |
|
Context.ReturnVarName); |
|
|
|
Context.Return.Write("({1})Marshal.GetDelegateForFunctionPointer({0}, typeof({1}))", |
|
ptrName, typedef.ToString()); |
|
return true; |
|
} |
|
|
|
return decl.Type.Visit(this); |
|
} |
|
|
|
public override bool VisitClassDecl(Class @class) |
|
{ |
|
var ctx = Context as CSharpMarshalContext; |
|
|
|
string instance = Context.ReturnVarName; |
|
|
|
if (@class.IsRefType && |
|
(Context.ReturnType.Qualifiers.IsConst || !Context.ReturnType.Type.IsAddress()) && |
|
(!Context.Driver.Options.GenerateAbstractImpls || !@class.IsAbstract)) |
|
{ |
|
var instanceName = Generator.GeneratedIdentifier("instance"); |
|
if (VarSuffix > 0) |
|
instanceName += VarSuffix; |
|
|
|
if (@class.HasNonTrivialCopyConstructor) |
|
{ |
|
// Find a valid copy constructor overload. |
|
var copyCtorMethod = @class.Methods.FirstOrDefault(method => |
|
method.IsCopyConstructor); |
|
|
|
if (copyCtorMethod == null) |
|
throw new NotSupportedException("Expected a valid copy constructor"); |
|
|
|
// Call the copy constructor. |
|
TypeMap typeMap; |
|
if (copyCtorMethod.Ignore && FindTypeMap(ctx.Driver.TypeDatabase, @class, out typeMap)) |
|
{ |
|
typeMap.CSharpMarshalCopyCtorToManaged(Context); |
|
} |
|
else |
|
{ |
|
// Allocate memory for a new native object and call the ctor. |
|
Context.SupportBefore.WriteLine("var {0} = Marshal.AllocHGlobal({1});", |
|
instanceName, @class.Layout.Size); |
|
Context.SupportBefore.WriteLine("{0}.Internal.{1}({2}, new global::System.IntPtr(&{3}));", |
|
QualifiedIdentifier(@class), |
|
CSharpTextTemplate.GetFunctionNativeIdentifier(copyCtorMethod), |
|
instanceName, instance); |
|
} |
|
} |
|
else |
|
{ |
|
// Allocate memory for a new native object and call the ctor. |
|
Context.SupportBefore.WriteLine("var {0} = Marshal.AllocHGlobal({1});", |
|
instanceName, @class.Layout.Size); |
|
instance = instance.Trim('*'); |
|
Context.SupportBefore.WriteLine( |
|
"CppSharp.Runtime.Helpers.memcpy({0}, new IntPtr(&{1}), new UIntPtr({2}));", |
|
instanceName, instance, @class.Layout.Size); |
|
} |
|
|
|
instance = instanceName; |
|
} |
|
|
|
if (@class.IsRefType) |
|
Context.Return.Write("({0} == IntPtr.Zero) ? null : ", |
|
instance); |
|
|
|
Context.Return.Write("new {0}({1})", |
|
QualifiedIdentifier(@class.OriginalClass ?? @class) + |
|
(Context.Driver.Options.GenerateAbstractImpls && @class.IsAbstract ? |
|
"Internal" : ""), |
|
instance); |
|
|
|
return true; |
|
} |
|
|
|
private static bool FindTypeMap(ITypeMapDatabase typeMapDatabase, |
|
Class @class, out TypeMap typeMap) |
|
{ |
|
return typeMapDatabase.FindTypeMap(@class, out typeMap) || |
|
(@class.HasBase && FindTypeMap(typeMapDatabase, @class.Bases[0].Class, out typeMap)); |
|
} |
|
|
|
public override bool VisitEnumDecl(Enumeration @enum) |
|
{ |
|
Context.Return.Write("{0}", Context.ReturnVarName); |
|
return true; |
|
} |
|
|
|
public override bool VisitParameterDecl(Parameter parameter) |
|
{ |
|
if (parameter.Usage == ParameterUsage.Unknown || parameter.IsIn) |
|
return base.VisitParameterDecl(parameter); |
|
|
|
var ctx = new CSharpMarshalContext(base.Context.Driver) |
|
{ |
|
ReturnType = base.Context.ReturnType, |
|
ReturnVarName = base.Context.ReturnVarName |
|
}; |
|
|
|
var marshal = new CSharpMarshalNativeToManagedPrinter(ctx); |
|
parameter.Type.Visit(marshal); |
|
|
|
if (!string.IsNullOrWhiteSpace(ctx.SupportBefore)) |
|
Context.SupportBefore.WriteLine(ctx.SupportBefore); |
|
|
|
if (!string.IsNullOrWhiteSpace(ctx.Return)) |
|
{ |
|
Context.SupportBefore.WriteLine("var _{0} = {1};", parameter.Name, |
|
ctx.Return); |
|
} |
|
|
|
Context.Return.Write("_{0}", parameter.Name); |
|
return true; |
|
} |
|
} |
|
|
|
public class CSharpMarshalManagedToNativePrinter : CSharpMarshalPrinter |
|
{ |
|
public CSharpMarshalManagedToNativePrinter(CSharpMarshalContext context) |
|
: base(context) |
|
{ |
|
Context.MarshalToNative = this; |
|
} |
|
|
|
public override bool VisitType(Type type, TypeQualifiers quals) |
|
{ |
|
TypeMap typeMap; |
|
if (Context.Driver.TypeDatabase.FindTypeMap(type, out typeMap) && typeMap.DoesMarshalling) |
|
{ |
|
typeMap.Type = type; |
|
typeMap.CSharpMarshalToNative(Context); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitDeclaration(Declaration decl) |
|
{ |
|
TypeMap typeMap; |
|
if (Context.Driver.TypeDatabase.FindTypeMap(decl, out typeMap) && typeMap.DoesMarshalling) |
|
{ |
|
typeMap.Declaration = decl; |
|
typeMap.CSharpMarshalToNative(Context); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitArrayType(ArrayType array, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(array, quals)) |
|
return false; |
|
|
|
switch (array.SizeType) |
|
{ |
|
case ArrayType.ArraySize.Constant: |
|
var supportBefore = Context.SupportBefore; |
|
supportBefore.WriteLine("if ({0} != null)", Context.ArgName); |
|
supportBefore.WriteStartBraceIndent(); |
|
supportBefore.WriteLine("for (int i = 0; i < {0}; i++)", array.Size); |
|
supportBefore.WriteLineIndent("{0}[i] = {1}[i];", |
|
Context.ReturnVarName, Context.ArgName); |
|
supportBefore.WriteCloseBraceIndent(); |
|
break; |
|
default: |
|
Context.Return.Write("null"); |
|
break; |
|
} |
|
return true; |
|
} |
|
|
|
public bool VisitDelegateType(FunctionType function, string type) |
|
{ |
|
Context.Return.Write("Marshal.GetFunctionPointerForDelegate({0})", |
|
Context.Parameter.Name); |
|
|
|
return true; |
|
} |
|
|
|
public override bool VisitPointerType(PointerType pointer, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(pointer, quals)) |
|
return false; |
|
|
|
var pointee = pointer.Pointee.Desugar(); |
|
|
|
if ((pointee.IsPrimitiveType(PrimitiveType.Char) || |
|
pointee.IsPrimitiveType(PrimitiveType.WideChar)) && |
|
pointer.QualifiedPointee.Qualifiers.IsConst) |
|
{ |
|
Context.Return.Write(MarshalStringToUnmanaged(Context.Parameter.Name)); |
|
CSharpContext.Cleanup.WriteLine("Marshal.FreeHGlobal({0});", Context.ArgName); |
|
return true; |
|
} |
|
|
|
if (pointee is FunctionType) |
|
{ |
|
var function = pointee as FunctionType; |
|
return VisitDelegateType(function, function.ToString()); |
|
} |
|
|
|
Class @class; |
|
if (pointee.IsTagDecl(out @class) && @class.IsValueType) |
|
{ |
|
if (Context.Parameter.Usage == ParameterUsage.Out) |
|
{ |
|
Context.SupportBefore.WriteLine("var {0} = new {1}.Internal();", |
|
Generator.GeneratedIdentifier(Context.ArgName), @class.Name); |
|
} |
|
else |
|
{ |
|
Context.SupportBefore.WriteLine("var {0} = {1}.ToInternal();", |
|
Generator.GeneratedIdentifier(Context.ArgName), |
|
Context.Parameter.Name); |
|
} |
|
|
|
Context.Return.Write("new global::System.IntPtr(&{0})", |
|
Generator.GeneratedIdentifier(Context.ArgName)); |
|
return true; |
|
} |
|
|
|
PrimitiveType primitive; |
|
if (pointee.IsPrimitiveType(out primitive)) |
|
{ |
|
var param = Context.Parameter; |
|
|
|
// From MSDN: "note that a ref or out parameter is classified as a moveable |
|
// variable". This means we must create a local variable to hold the result |
|
// and then assign this value to the parameter. |
|
|
|
if (param.IsOut || param.IsInOut) |
|
{ |
|
var typeName = Type.TypePrinterDelegate(pointee); |
|
|
|
Context.SupportBefore.WriteLine("{0} _{1};", typeName, param.Name); |
|
Context.Return.Write("&_{0}", param.Name); |
|
|
|
} |
|
else |
|
Context.Return.Write(Context.Parameter.Name); |
|
|
|
return true; |
|
} |
|
|
|
return pointer.Pointee.Visit(this, quals); |
|
} |
|
|
|
private string MarshalStringToUnmanaged(string varName) |
|
{ |
|
if (Equals(Context.Driver.Options.Encoding, Encoding.ASCII)) |
|
{ |
|
return string.Format("Marshal.StringToHGlobalAnsi({0})", varName); |
|
} |
|
if (Equals(Context.Driver.Options.Encoding, Encoding.Unicode) || |
|
Equals(Context.Driver.Options.Encoding, Encoding.BigEndianUnicode)) |
|
{ |
|
return string.Format("Marshal.StringToHGlobalUni({0})", varName); |
|
} |
|
throw new NotSupportedException(string.Format("{0} is not supported yet.", |
|
Context.Driver.Options.Encoding.EncodingName)); |
|
} |
|
|
|
public override bool VisitPrimitiveType(PrimitiveType primitive, TypeQualifiers quals) |
|
{ |
|
switch (primitive) |
|
{ |
|
case PrimitiveType.Void: |
|
return true; |
|
case PrimitiveType.Bool: |
|
case PrimitiveType.Int8: |
|
case PrimitiveType.UInt8: |
|
case PrimitiveType.Int16: |
|
case PrimitiveType.UInt16: |
|
case PrimitiveType.Int32: |
|
case PrimitiveType.UInt32: |
|
case PrimitiveType.Int64: |
|
case PrimitiveType.UInt64: |
|
case PrimitiveType.Float: |
|
case PrimitiveType.Double: |
|
Context.Return.Write(Context.Parameter.Name); |
|
return true; |
|
case PrimitiveType.Char16: |
|
case PrimitiveType.WideChar: |
|
return false; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
public override bool VisitTypedefType(TypedefType typedef, TypeQualifiers quals) |
|
{ |
|
if (!VisitType(typedef, quals)) |
|
return false; |
|
|
|
var decl = typedef.Declaration; |
|
|
|
FunctionType func; |
|
if (decl.Type.IsPointerTo<FunctionType>(out func)) |
|
{ |
|
VisitDelegateType(func, typedef.Declaration.OriginalName); |
|
return true; |
|
} |
|
|
|
return decl.Type.Visit(this); |
|
} |
|
|
|
public override bool VisitTemplateParameterType(TemplateParameterType param, TypeQualifiers quals) |
|
{ |
|
Context.Return.Write(param.Parameter.Name); |
|
return true; |
|
} |
|
|
|
public override bool VisitClassDecl(Class @class) |
|
{ |
|
if (!VisitDeclaration(@class)) |
|
return false; |
|
|
|
if (@class.IsValueType) |
|
{ |
|
MarshalValueClass(); |
|
} |
|
else |
|
{ |
|
MarshalRefClass(@class); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
private void MarshalRefClass(Class @class) |
|
{ |
|
var method = Context.Function as Method; |
|
if (method != null |
|
&& method.Conversion == MethodConversionKind.FunctionToInstanceMethod |
|
&& Context.ParameterIndex == 0) |
|
{ |
|
Context.Return.Write("{0}", Helpers.InstanceIdentifier); |
|
return; |
|
} |
|
|
|
string param = Context.Parameter.Name; |
|
Type type = Context.Parameter.Type.Desugar(); |
|
if (type.IsAddress()) |
|
{ |
|
Class decl; |
|
if (type.IsTagDecl(out decl) && decl.IsValueType) |
|
Context.Return.Write("{0}.{1}", param, Helpers.InstanceIdentifier); |
|
else |
|
Context.Return.Write("{0} == ({2}) null ? global::System.IntPtr.Zero : {0}.{1}", param, |
|
Helpers.InstanceIdentifier, type); |
|
return; |
|
} |
|
|
|
var qualifiedIdentifier = CSharpMarshalNativeToManagedPrinter.QualifiedIdentifier( |
|
@class.OriginalClass ?? @class); |
|
Context.Return.Write( |
|
"ReferenceEquals({0}, null) ? new {1}.Internal() : *({1}.Internal*) ({0}.{2})", param, |
|
qualifiedIdentifier, Helpers.InstanceIdentifier); |
|
} |
|
|
|
private void MarshalValueClass() |
|
{ |
|
Context.Return.Write("{0}.ToInternal()", Context.Parameter.Name); |
|
} |
|
|
|
public override bool VisitFieldDecl(Field field) |
|
{ |
|
if (!VisitDeclaration(field)) |
|
return false; |
|
|
|
Context.Parameter = new Parameter |
|
{ |
|
Name = Context.ArgName, |
|
QualifiedType = field.QualifiedType |
|
}; |
|
|
|
return field.Type.Visit(this, field.QualifiedType.Qualifiers); |
|
} |
|
|
|
public override bool VisitProperty(Property property) |
|
{ |
|
if (!VisitDeclaration(property)) |
|
return false; |
|
|
|
Context.Parameter = new Parameter |
|
{ |
|
Name = Context.ArgName, |
|
QualifiedType = property.QualifiedType |
|
}; |
|
|
|
return base.VisitProperty(property); |
|
} |
|
|
|
public override bool VisitEnumDecl(Enumeration @enum) |
|
{ |
|
Context.Return.Write(Context.Parameter.Name); |
|
return true; |
|
} |
|
|
|
public override bool VisitClassTemplateDecl(ClassTemplate template) |
|
{ |
|
return VisitClassDecl(template.TemplatedClass); |
|
} |
|
|
|
public override bool VisitFunctionTemplateDecl(FunctionTemplate template) |
|
{ |
|
return template.TemplatedFunction.Visit(this); |
|
} |
|
} |
|
|
|
public static class CSharpMarshalExtensions |
|
{ |
|
public static void CSharpMarshalToNative(this QualifiedType type, |
|
CSharpMarshalManagedToNativePrinter printer) |
|
{ |
|
(printer.Context as CSharpMarshalContext).FullType = type; |
|
type.Visit(printer); |
|
} |
|
|
|
public static void CSharpMarshalToNative(this Type type, |
|
CSharpMarshalManagedToNativePrinter printer) |
|
{ |
|
CSharpMarshalToNative(new QualifiedType(type), printer); |
|
} |
|
|
|
public static void CSharpMarshalToNative(this ITypedDecl decl, |
|
CSharpMarshalManagedToNativePrinter printer) |
|
{ |
|
CSharpMarshalToNative(decl.QualifiedType, printer); |
|
} |
|
|
|
public static void CSharpMarshalToManaged(this QualifiedType type, |
|
CSharpMarshalNativeToManagedPrinter printer) |
|
{ |
|
(printer.Context as CSharpMarshalContext).FullType = type; |
|
type.Visit(printer); |
|
} |
|
|
|
public static void CSharpMarshalToManaged(this Type type, |
|
CSharpMarshalNativeToManagedPrinter printer) |
|
{ |
|
CSharpMarshalToManaged(new QualifiedType(type), printer); |
|
} |
|
|
|
public static void CSharpMarshalToManaged(this ITypedDecl decl, |
|
CSharpMarshalNativeToManagedPrinter printer) |
|
{ |
|
CSharpMarshalToManaged(decl.QualifiedType, printer); |
|
} |
|
} |
|
}
|
|
|