Merge pull request #537 from tritao/swift-improvements
[swift] Swift backend implementation improvements
This commit is contained in:
Коммит
36e32c87bf
|
@ -178,7 +178,8 @@ namespace Embeddinator
|
|||
}
|
||||
|
||||
//NOTE: Choosing Java generator, needs to imply the C generator
|
||||
if (Generators.Contains(GeneratorKind.Java) && !Generators.Contains(GeneratorKind.C))
|
||||
if ((Generators.Contains(GeneratorKind.Java) || Generators.Contains(GeneratorKind.Swift)) &&
|
||||
!Generators.Contains(GeneratorKind.C))
|
||||
{
|
||||
Generators.Insert(0, GeneratorKind.C);
|
||||
}
|
||||
|
|
|
@ -95,19 +95,35 @@ namespace Embeddinator
|
|||
"Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs");
|
||||
var sdk = Directory.EnumerateDirectories(sdkPath).First();
|
||||
|
||||
var args = new List<string> {
|
||||
"-emit-module",
|
||||
$"-emit-module-path {Options.OutputDir}",
|
||||
$"-module-name {moduleName}",
|
||||
$"-swift-version {swiftVersion}",
|
||||
$"-sdk {sdk}",
|
||||
string.Join(" ", files.ToList())
|
||||
};
|
||||
bool compileSuccess = true;
|
||||
|
||||
var invocation = string.Join(" ", args);
|
||||
var output = Invoke(swiftcBin, invocation);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var args = new List<string>
|
||||
{
|
||||
"-emit-module",
|
||||
$"-emit-module-path {Options.OutputDir}",
|
||||
$"-module-name {moduleName}",
|
||||
$"-swift-version {swiftVersion}",
|
||||
$"-sdk {sdk}",
|
||||
$"-I \"{MonoSdkPath}/include/mono-2.0\"",
|
||||
};
|
||||
|
||||
return output.ExitCode == 0;
|
||||
var bridgingHeader = Directory.EnumerateFiles(Options.OutputDir, "*.h")
|
||||
.SingleOrDefault(header => Path.GetFileNameWithoutExtension(header) ==
|
||||
Path.GetFileNameWithoutExtension(file));
|
||||
|
||||
args.Add($"-import-objc-header {bridgingHeader}");
|
||||
|
||||
args.Add(file);
|
||||
|
||||
var invocation = string.Join(" ", args);
|
||||
var output = Invoke(swiftcBin, invocation);
|
||||
|
||||
compileSuccess &= output.ExitCode == 0;
|
||||
}
|
||||
|
||||
return compileSuccess;
|
||||
}
|
||||
|
||||
bool CompileJava(IEnumerable<string> files)
|
||||
|
|
|
@ -426,7 +426,7 @@ namespace Embeddinator.Generators
|
|||
}
|
||||
else if (managedType.IsArray)
|
||||
{
|
||||
if (Options.GeneratorKind == GeneratorKind.Java)
|
||||
if (Options.GeneratorKind == GeneratorKind.Java || Options.GeneratorKind == GeneratorKind.Swift)
|
||||
return new QualifiedType(new UnsupportedType { Description = managedType.FullName });
|
||||
|
||||
var array = new ArrayType
|
||||
|
|
|
@ -152,9 +152,6 @@ namespace Embeddinator.Generators
|
|||
case PrimitiveType.ULong: return "UnsignedLong";
|
||||
case PrimitiveType.LongLong: return "LongLong";
|
||||
case PrimitiveType.ULongLong: return "UnsignedLongLong";
|
||||
case PrimitiveType.Int128: return "__int128";
|
||||
case PrimitiveType.UInt128: return "__uint128_t";
|
||||
case PrimitiveType.Half: return "__fp16";
|
||||
case PrimitiveType.Float: return useReferencePrimitiveTypes ? "Float" : "float";
|
||||
case PrimitiveType.Double: return useReferencePrimitiveTypes ? "Double" : "double";
|
||||
case PrimitiveType.IntPtr:
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CppSharp;
|
||||
using CppSharp.AST;
|
||||
using CppSharp.Generators;
|
||||
using CppSharp.Passes;
|
||||
|
||||
namespace Embeddinator.Generators
|
||||
{
|
||||
|
@ -11,17 +9,12 @@ namespace Embeddinator.Generators
|
|||
{
|
||||
public SwiftTypePrinter TypePrinter { get; internal set; }
|
||||
|
||||
public static string IntPtrType = "UnsafePointer<Void>";
|
||||
|
||||
PassBuilder<TranslationUnitPass> Passes;
|
||||
public static string IntPtrType = "UnsafeRawPointer";
|
||||
|
||||
public SwiftGenerator(BindingContext context)
|
||||
: base(context)
|
||||
{
|
||||
TypePrinter = new SwiftTypePrinter(Context);
|
||||
|
||||
Passes = new PassBuilder<TranslationUnitPass>(Context);
|
||||
CGenerator.SetupPasses(Passes);
|
||||
}
|
||||
|
||||
public override List<CodeGenerator> Generate(IEnumerable<TranslationUnit> units)
|
||||
|
@ -29,16 +22,7 @@ namespace Embeddinator.Generators
|
|||
var unit = units.First();
|
||||
var sources = new SwiftSources(Context, unit);
|
||||
|
||||
// Also generate a separate file with equivalent of P/Invoke declarations.
|
||||
var nativeSources = GenerateNativeDeclarations(unit);
|
||||
|
||||
return new List<CodeGenerator> { sources, nativeSources };
|
||||
}
|
||||
|
||||
public CodeGenerator GenerateNativeDeclarations(TranslationUnit unit)
|
||||
{
|
||||
CGenerator.RunPasses(Context, Passes);
|
||||
return new SwiftNative(Context, unit);
|
||||
return new List<CodeGenerator> { sources };
|
||||
}
|
||||
|
||||
public override bool SetupPasses()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using CppSharp.AST;
|
||||
using CppSharp.AST.Extensions;
|
||||
|
||||
namespace Embeddinator.Generators
|
||||
{
|
||||
|
@ -36,24 +35,74 @@ namespace Embeddinator.Generators
|
|||
|
||||
public override bool VisitClassDecl(Class @class)
|
||||
{
|
||||
Context.Return.Write($"{Context.ArgName}");
|
||||
var objectRef = @class.IsInterface ? "__getObject()" : "__object!";
|
||||
Context.Return.Write($"{Context.ArgName}.{objectRef}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HandleRefOutPrimitiveType(PrimitiveType type, Enumeration @enum = null)
|
||||
{
|
||||
Context.Return.Write(Context.ArgName);
|
||||
}
|
||||
|
||||
public override bool VisitEnumDecl(Enumeration @enum)
|
||||
{
|
||||
Context.Return.Write(Context.ArgName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandleDecimalType()
|
||||
{
|
||||
var decimalId = SwiftGenerator.GeneratedIdentifier($"{Context.Parameter.Name}_decimal");
|
||||
var @var = IsByRefParameter ? "var" : "let";
|
||||
Context.SupportBefore.WriteLine($"{@var} {decimalId} : MonoDecimal = mono_embeddinator_string_to_decimal(\"\")");
|
||||
|
||||
var pointerId = "pointer";
|
||||
if (IsByRefParameter)
|
||||
{
|
||||
Context.SupportBefore.WriteLine($"withUnsafeMutablePointer(to: &{decimalId}) {{ ({pointerId}) in");
|
||||
Context.SupportAfter.WriteLine("}");
|
||||
}
|
||||
|
||||
Context.Return.Write(IsByRefParameter ? pointerId : decimalId);
|
||||
}
|
||||
|
||||
public void HandleRefOutPrimitiveType(PrimitiveType type)
|
||||
{
|
||||
if (type == PrimitiveType.String)
|
||||
{
|
||||
var gstringId = $"{Context.ReturnVarName}_gstring";
|
||||
Context.SupportBefore.WriteLine($"let {gstringId} : UnsafeMutablePointer<GString> = g_string_new(\"\")");
|
||||
|
||||
Context.SupportBefore.WriteLine($"g_string_free({gstringId}, 1)");
|
||||
|
||||
Context.Return.Write(gstringId);
|
||||
return;
|
||||
}
|
||||
else if (type == PrimitiveType.Decimal)
|
||||
{
|
||||
HandleDecimalType();
|
||||
return;
|
||||
}
|
||||
|
||||
Context.Return.Write($"&{Context.ArgName}");
|
||||
}
|
||||
|
||||
public override bool VisitPrimitiveType(PrimitiveType type,
|
||||
TypeQualifiers quals)
|
||||
{
|
||||
if (IsByRefParameter)
|
||||
{
|
||||
HandleRefOutPrimitiveType(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == PrimitiveType.Char)
|
||||
{
|
||||
Context.Return.Write($"gunichar2({Context.ArgName}.unicodeScalars.first!.value)");
|
||||
return true;
|
||||
}
|
||||
else if (type == PrimitiveType.Decimal)
|
||||
{
|
||||
HandleDecimalType();
|
||||
return true;
|
||||
}
|
||||
|
||||
Context.Return.Write(Context.ArgName);
|
||||
return true;
|
||||
}
|
||||
|
@ -82,7 +131,15 @@ namespace Embeddinator.Generators
|
|||
|
||||
public override bool VisitClassDecl(Class @class)
|
||||
{
|
||||
Context.Return.Write(Context.ReturnVarName);
|
||||
var typePrinter = new SwiftTypePrinter(Context.Context);
|
||||
var typeName = @class.Visit(typePrinter);
|
||||
|
||||
//if (@class.IsInterface || @class.IsAbstract)
|
||||
//typeName = $"{typeName}Impl";
|
||||
|
||||
Context.Return.Write($"{typeName}()");
|
||||
|
||||
//Context.Return.Write(Context.ReturnVarName);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -95,8 +152,41 @@ namespace Embeddinator.Generators
|
|||
public override bool VisitPrimitiveType(PrimitiveType type,
|
||||
TypeQualifiers quals)
|
||||
{
|
||||
if (type == PrimitiveType.Char)
|
||||
{
|
||||
Context.Return.Write($"Character(Unicode.Scalar({Context.ReturnVarName})!)");
|
||||
return true;
|
||||
}
|
||||
else if (type == PrimitiveType.String)
|
||||
{
|
||||
Context.Return.Write($"String(cString: {Context.ReturnVarName})");
|
||||
return true;
|
||||
}
|
||||
else if (type == PrimitiveType.Decimal)
|
||||
{
|
||||
HandleDecimalType();
|
||||
return true;
|
||||
}
|
||||
|
||||
Context.Return.Write(Context.ReturnVarName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandleDecimalType()
|
||||
{
|
||||
var gstringId = $"{Context.ReturnVarName}_gstring";
|
||||
Context.SupportBefore.Write($"let {gstringId} : UnsafeMutablePointer<GString> = ");
|
||||
Context.SupportBefore.WriteLine($"mono_embeddinator_decimal_to_gstring({Context.ReturnVarName})");
|
||||
|
||||
var stringId = $"{Context.ReturnVarName}_string";
|
||||
Context.SupportBefore.WriteLine($"let {stringId} : String = String(cString: {gstringId}.pointee.str)");
|
||||
|
||||
var decimalId = $"{Context.ReturnVarName}_decimal";
|
||||
Context.SupportBefore.WriteLine($"let {decimalId} : Decimal = Decimal(string: {stringId})!");
|
||||
|
||||
Context.SupportBefore.WriteLine($"g_string_free({gstringId}, 1)");
|
||||
|
||||
Context.Return.Write(decimalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CppSharp;
|
||||
using CppSharp.AST;
|
||||
using CppSharp.Generators;
|
||||
|
||||
namespace Embeddinator.Generators
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is responsible for generating JNA-compatible method and class
|
||||
/// Java code for a given managed library represented as a translation unit.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Unit = {TranslationUnit}")]
|
||||
public class SwiftNative : SwiftSources
|
||||
{
|
||||
public SwiftNative(BindingContext context, TranslationUnit unit)
|
||||
: base(context, unit)
|
||||
{
|
||||
}
|
||||
|
||||
public static string GetNativeLibClassName(TranslationUnit unit) =>
|
||||
GetNativeLibClassName(unit.FileName);
|
||||
|
||||
public static string GetNativeLibClassName(string fileName) =>
|
||||
$"Native_{JavaGenerator.FileNameAsIdentifier(fileName)}";
|
||||
|
||||
public string ClassName => GetNativeLibClassName(TranslationUnit);
|
||||
|
||||
public override string FilePath => $"{ClassName}.{FileExtension}";
|
||||
|
||||
public override void Process()
|
||||
{
|
||||
GenerateFilePreamble(CommentKind.JavaDoc, "Embeddinator-4000");
|
||||
|
||||
GenerateImports();
|
||||
|
||||
TranslationUnit.Visit(this);
|
||||
}
|
||||
|
||||
public override bool VisitTranslationUnit(TranslationUnit unit)
|
||||
{
|
||||
Write($"public class {ClassName} ");
|
||||
WriteStartBraceIndent();
|
||||
|
||||
var ret = base.VisitTranslationUnit(unit);
|
||||
|
||||
WriteCloseBraceIndent();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static IEnumerable<Declaration> GetOverloadedDeclarations(Declaration decl)
|
||||
{
|
||||
var @class = decl.Namespace as Class;
|
||||
return @class.Declarations.Where(d => d.OriginalName == decl.OriginalName);
|
||||
}
|
||||
|
||||
public override bool VisitMethodDecl(Method method)
|
||||
{
|
||||
if (!VisitDeclaration(method))
|
||||
return false;
|
||||
|
||||
if (method.IsImplicit)
|
||||
return false;
|
||||
|
||||
PushBlock(BlockKind.Method, method);
|
||||
|
||||
TypePrinter.PushContext(TypePrinterContextKind.Native);
|
||||
|
||||
var returnTypeName = method.ReturnType.Visit(TypePrinter);
|
||||
Write($"public static func {JavaNative.GetCMethodIdentifier(method)}(");
|
||||
Write(TypePrinter.VisitParameters(method.Parameters, hasNames: true).ToString());
|
||||
Write($") -> {returnTypeName};");
|
||||
|
||||
TypePrinter.PopContext();
|
||||
|
||||
PopBlock(NewLineKind.Never);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool VisitClassDecl(Class @class)
|
||||
{
|
||||
if (!VisitDeclaration(@class))
|
||||
return false;
|
||||
|
||||
VisitDeclContext(@class);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool VisitEnumDecl(Enumeration @enum)
|
||||
{
|
||||
return VisitDeclaration(@enum);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CppSharp;
|
||||
|
@ -172,6 +173,21 @@ namespace Embeddinator.Generators
|
|||
Write(" ");
|
||||
WriteStartBraceIndent();
|
||||
|
||||
var hasNonInterfaceBase = @class.HasBaseClass && @class.BaseClass.IsGenerated
|
||||
&& !@class.BaseClass.IsInterface;
|
||||
|
||||
var objectIdent = SwiftGenerator.GeneratedIdentifier("object");
|
||||
|
||||
if (!@class.IsStatic && !@class.IsInterface && !hasNonInterfaceBase)
|
||||
{
|
||||
TypePrinter.PushContext(TypePrinterContextKind.Native);
|
||||
var typeName = @class.Visit(TypePrinter);
|
||||
TypePrinter.PopContext();
|
||||
|
||||
WriteLine($"public var {objectIdent} : {typeName}");
|
||||
NewLine();
|
||||
}
|
||||
|
||||
VisitDeclContext(@class);
|
||||
WriteCloseBraceIndent();
|
||||
PopBlock(NewLineKind.BeforeNextBlock);
|
||||
|
@ -277,6 +293,47 @@ namespace Embeddinator.Generators
|
|||
|
||||
@params.Add(marshal.Context.Return);
|
||||
}
|
||||
|
||||
var hasReturn = !method.ReturnType.Type.IsPrimitiveType(PrimitiveType.Void) &&
|
||||
!(method.IsConstructor || method.IsDestructor);
|
||||
|
||||
if (hasReturn)
|
||||
{
|
||||
TypePrinter.PushContext(TypePrinterContextKind.Native);
|
||||
var typeName = method.ReturnType.Visit(TypePrinter);
|
||||
TypePrinter.PopContext();
|
||||
Write($"let __ret : {typeName.Type} = ");
|
||||
}
|
||||
|
||||
var effectiveMethod = method.CompleteDeclaration as Method ?? method;
|
||||
var nativeMethodId = JavaNative.GetCMethodIdentifier(effectiveMethod);
|
||||
WriteLine($"{nativeMethodId}({string.Join(", ", @params)})");
|
||||
|
||||
foreach (var marshal in contexts)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(marshal.SupportAfter))
|
||||
Write(marshal.SupportAfter);
|
||||
}
|
||||
|
||||
if (hasReturn)
|
||||
{
|
||||
var ctx = new MarshalContext(Context)
|
||||
{
|
||||
ReturnType = method.ReturnType,
|
||||
ReturnVarName = "__ret"
|
||||
};
|
||||
|
||||
var marshal = new SwiftMarshalNativeToManaged(ctx);
|
||||
method.ReturnType.Visit(marshal);
|
||||
|
||||
if (marshal.Context.Return.ToString().Length == 0)
|
||||
throw new NotSupportedException($"Cannot marshal return type {method.ReturnType}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(marshal.Context.SupportBefore))
|
||||
Write(marshal.Context.SupportBefore);
|
||||
|
||||
WriteLine($"return {marshal.Context.Return}");
|
||||
}
|
||||
}
|
||||
|
||||
public override bool VisitTypedefDecl(TypedefDecl typedef)
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace Embeddinator.Generators
|
|||
public override TypePrinterResult VisitClassDecl(Class @class)
|
||||
{
|
||||
if (ContextKind == TypePrinterContextKind.Native)
|
||||
return VisitPrimitiveType(PrimitiveType.IntPtr);
|
||||
return $"UnsafeMutablePointer<{CGenerator.QualifiedName(@class)}>!";
|
||||
|
||||
return VisitDeclaration(@class);
|
||||
}
|
||||
|
@ -37,11 +37,13 @@ namespace Embeddinator.Generators
|
|||
public override TypePrinterResult VisitParameter(Parameter param, bool hasName)
|
||||
{
|
||||
Parameter = param;
|
||||
var type = param.QualifiedType.Visit(this);
|
||||
|
||||
var name = hasName ? $"{param.Name}" : string.Empty;
|
||||
var inout = IsByRefParameter ? "inout " : string.Empty;
|
||||
var type = param.QualifiedType.Visit(this);
|
||||
|
||||
Parameter = null;
|
||||
|
||||
var inout = IsByRefParameter ? "inout " : string.Empty;
|
||||
return $"{name} : {inout}{type}";
|
||||
}
|
||||
|
||||
|
@ -57,6 +59,9 @@ namespace Embeddinator.Generators
|
|||
{
|
||||
var isNative = ContextKind == TypePrinterContextKind.Native;
|
||||
|
||||
if (isNative && IsByRefParameter && primitive == PrimitiveType.String)
|
||||
return "GString";
|
||||
|
||||
switch (primitive)
|
||||
{
|
||||
case PrimitiveType.Bool: return isNative ? "CBool" : "Bool";
|
||||
|
@ -64,27 +69,24 @@ namespace Embeddinator.Generators
|
|||
case PrimitiveType.Char16: return "CChar16";
|
||||
case PrimitiveType.Char32: return "CChar32";
|
||||
case PrimitiveType.WideChar: return "CWideChar";
|
||||
case PrimitiveType.Char: return "Character";
|
||||
case PrimitiveType.Char: return isNative ? "gunichar2" : "Character";
|
||||
case PrimitiveType.SChar: return isNative ? "CChar" : "Int8";
|
||||
case PrimitiveType.UChar: return isNative ? "CUnsignedChar" : "UInt8";
|
||||
case PrimitiveType.Short: return isNative ? "CShort" : "Int16";
|
||||
case PrimitiveType.UShort: return isNative ? "CUnsignedShort" : "UInt16";
|
||||
case PrimitiveType.Int: return isNative ? "CInt" : "Int32";
|
||||
case PrimitiveType.UInt: return isNative ? "CUnsignedInt" : "UInt32";
|
||||
case PrimitiveType.Long: return isNative ? "CLong" : "Int64";
|
||||
case PrimitiveType.ULong: return isNative ? "CUnsignedLong" : "UInt64";
|
||||
case PrimitiveType.Long: return isNative ? "CLongLong" : "Int64";
|
||||
case PrimitiveType.ULong: return isNative ? "CUnsignedLongLong" : "UInt64";
|
||||
case PrimitiveType.LongLong: return isNative ? "CLongLong" : "LongLong";
|
||||
case PrimitiveType.ULongLong: return isNative ? "CUnsignedLongLong" : "UnsignedLongLong";
|
||||
case PrimitiveType.Int128: return "__int128";
|
||||
case PrimitiveType.UInt128: return "__uint128_t";
|
||||
case PrimitiveType.Half: return "__fp16";
|
||||
case PrimitiveType.Float: return isNative ? "CFloat" : "Float";
|
||||
case PrimitiveType.Double: return isNative ? "CDouble" : "Double";
|
||||
case PrimitiveType.IntPtr:
|
||||
case PrimitiveType.UIntPtr:
|
||||
case PrimitiveType.Null: return SwiftGenerator.IntPtrType;
|
||||
case PrimitiveType.String: return "String";
|
||||
case PrimitiveType.Decimal: return "Decimal";
|
||||
case PrimitiveType.String: return isNative ? "UnsafePointer<CChar>" : "String";
|
||||
case PrimitiveType.Decimal: return isNative ? "MonoDecimal" : "Decimal";
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
|
|
|
@ -123,6 +123,9 @@
|
|||
<Compile Include="../../binder/Generators/Swift/SwiftGenerator.cs">
|
||||
<Link>binder/Generators/Swift/SwiftGenerator.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="../../binder/Generators/Swift/SwiftMarshal.cs">
|
||||
<Link>binder/Generators/Swift/SwiftMarshal.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="../../binder/Generators/Swift/SwiftSources.cs">
|
||||
<Link>binder/Generators/Swift/SwiftSources.cs</Link>
|
||||
</Compile>
|
||||
|
@ -193,12 +196,6 @@
|
|||
<Link>binder/Utils/XamarinAndroidBuild.cs</Link>
|
||||
</Compile>
|
||||
<None Include="packages.config" />
|
||||
<Compile Include="..\..\binder\Generators\Swift\SwiftMarshal.cs">
|
||||
<Link>binder\Generators\Swift\SwiftMarshal.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\..\binder\Generators\Swift\SwiftNative.cs">
|
||||
<Link>binder\Generators\Swift\SwiftNative.cs</Link>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="IKVM.Reflection.csproj">
|
||||
|
|
Загрузка…
Ссылка в новой задаче