Azure Functions with Swift

I’ve been a bit busy lately with several projects, but I’ve tried to carve out time to continue learning. As I’ve been writing some Azure Functions for work projects, I was looking to leverage see how easy it would be to use the Custom Handlers preview to add support for Swift. Turns out Saleh Albuga already built a tool for building and deploying Swift Azure Functions called swiftfunc! The only downside is this tool currently only works on macOS.

Undaunted, I decided to try a different approach. While looking for OSS Swift compilers, I happened upon RemObjects Elements Compiler. I was surprised I had not heard of them before. Their compiler platform is worth investigating, as it supports many languages, even within one project! However, I was interested in Swift, and their Silver compiler is kept very close to the latest Swift spec, including extensions for things like async/await. As the Elements compiler can be used to build apps for mobile, .NET, Java, WebAssembly, and more platforms, I wondered whether I could use Silver to build a Swift Azure Function against the .NET libraries. With a little help from Marc Hoffman, the answer is a resounding YES!

In this post, we’ll build an Azure Function that uses the dotnet runtime with the Swift language. We’ll follow the quickstart example from the docs, which you can find here.

Creating the Project

In order to generate a dotnet Azure Function with RemObjects, we’ll need to modify the project file. Since a dotnet Azure Function is just a class library, the best way to start is to generate a new Class Library (.NET Standard) project in either Water or Visual Studio (on Windows; Fire if on a Mac). You can download the Elements Compiler and IDE for your environment here.

Project options in Water
Starting project in Water
Project options in Visual Studio 2019
Starting project in Visual Studio 2019

This will generate a new .elements project file with something like the following contents:

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <PropertyGroup>
    <ProductVersion>3.5</ProductVersion>
    <RootNamespace>silverfunc</RootNamespace>
    <ProjectGuid>{796792DD-F4C0-484D-9DA7-98D793F081B6}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AssemblyName>silverfunc</AssemblyName>
    <Configuration Condition="'$(Configuration)' == ''">Release</Configuration>
    <TargetFramework>.NETStandard</TargetFramework>
    <Mode>Echoes</Mode>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
    <Optimize>False</Optimize>
    <OutputPath>.\Bin\Debug</OutputPath>
    <DefineConstants>DEBUG;TRACE;</DefineConstants>
    <GeneratePDB>True</GeneratePDB>
    <GenerateMDB>True</GenerateMDB>
    <EnableAsserts>True</EnableAsserts>
    <CpuType>anycpu</CpuType>
    <EnableUnmanagedDebugging>False</EnableUnmanagedDebugging>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
    <OutputPath>.\Bin\Release</OutputPath>
    <CpuType>anycpu</CpuType>
    <EnableUnmanagedDebugging>False</EnableUnmanagedDebugging>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Swift">
      <Private>True</Private>
    </Reference>
    <Reference Include="Echoes">
      <Private>True</Private>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Class1.swift" />
    <Compile Include="Properties\AssemblyInfo.swift" />
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
    </EmbeddedResource>
    <Compile Include="Properties\Resources.Designer.swift" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
    </None>
    <Compile Include="Properties\Settings.Designer.swift" />
  </ItemGroup>
  <Import Project="$(MSBuildExtensionsPath)\RemObjects Software\Elements\RemObjects.Elements.targets" />
</Project>

We’ll need to edit multiple parts of this file. If you are working in Water, you need to open the file in a different editor, as Water doesn’t seem to allow you to edit the .elements file directly. I’m not sure about Fire.

Also, while the file looks a lot like an MSBuild file, it’s actually an EBuild file. You can read up more on EBuild here.

First things first, we need to replace the contents of the top PropertyGroup and remove the remaining PropertyGroup sections.

  <PropertyGroup>
    <TargetFramework>.NETCore3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <RootNamespace>silverfunc</RootNamespace>
    <Mode>Echoes</Mode>
    <ProjectGuid>{796792DD-F4C0-484D-9DA7-98D793F081B6}</ProjectGuid>
    <OutputType>Library</OutputType>
    <OutputPath>.\bin\output</OutputPath>
  </PropertyGroup>

To be fair, we don’t need to set the OutputPath to bin/output, but that’s the norm for dotnet Azure Functions projects, so I did it here for consistency. We are essentially changing the TargetFramework, adding the AzureFunctionsVersion element, and removing a bunch of stuff that isn’t necessary.

Next, we need to clean up the ItemGroup elements. We only need one, so you can remove the </ItemGroup><ItemGroup> in the middle unless you just like separate sections for references and files.

  <ItemGroup>
    <NuGetReference Include="Microsoft.NET.Sdk.Functions:3.0.1">
      <Version>3.0.1</Version>
    </NuGetReference>
    <Reference Include="Swift">
      <Private>True</Private>
    </Reference>
    <Reference Include="Echoes">
      <Private>True</Private>
    </Reference>
    <Compile Include="Class.swift" />
  </ItemGroup>

As you can see, we added a NuGetReference to Microsoft.NET.Sdk.Functions 3.0.1. We also again removed a bunch of files that are not necessary. Your .elements file should now look like this:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
  <PropertyGroup>
    <TargetFramework>.NETCore3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <RootNamespace>silverfunc</RootNamespace>
    <Mode>Echoes</Mode>
    <ProjectGuid>{796792DD-F4C0-484D-9DA7-98D793F081B6}</ProjectGuid>
    <OutputType>Library</OutputType>
    <OutputPath>.\bin\output</OutputPath>
  </PropertyGroup>
  <ItemGroup>
    <NuGetReference Include="Microsoft.NET.Sdk.Functions:3.0.1">
      <Version>3.0.1</Version>
    </NuGetReference>
    <Reference Include="Swift">
      <Private>True</Private>
    </Reference>
    <Reference Include="Echoes">
      <Private>True</Private>
    </Reference>
    <Compile Include="Class.swift" />
  </ItemGroup>
  <Import Project="$(MSBuildExtensionsPath)\RemObjects Software\Elements\RemObjects.Elements.Echoes.targets" />
</Project>

We are now ready to start building our Azure Function. Since this is not using the dotnet compiler but an alternative tool, we will not be able to use attributes to generate the function.json bindings. We’ll have to add that just as we would for a Node.js, Python, etc. project. We also need to add a host.json file. A minimal host.json is simply

{
    "version": "2.0"
}

It’s easy to add this through either Water or Visual Studio and set it to Build Action: None and Copy to Output: PreserveNewest.

Let’s rename Class.swift to HttpExample.swift, as in the quickstart example. We also need to add a physical folder named HttpExample and then add a function.json to that folder. Set the function.json properties the same way you set host.json. This will set up the appropriate function binding. The HttpExample.swift can be stored anywhere since it will be compiled. I’ve left it in the root of my project, but you should be able to move it to the HttpExample folder if you want. Our binding is going to be a simple httpTrigger:

{
  "bindings": [
    {
      "type": "httpTrigger",
      "methods": [
        "get",
        "post"
      ],
      "authLevel": "function",
      "name": "req"
    }
  ],
  "disabled": false,
  "scriptFile": "../silverfunc.dll",
  "entryPoint": "silverfunc.HttpExample.Run"
}

You may wonder where the scriptFile and entryPoint come from, especially since we haven’t even written any code. This is essentially a copy of the generated function.json from a similar C# project. The compiled library is typically published in the bin/output root, and the function bindings are in folders named after the function with a function.json as the only content. The entryPoint is derived from the RootNamespace from the .elements project file and the class and method names we’ll define next.

I don’t know Swift that well, so I used Water to convert the C# version of the function to Swift for me.

Add and convert a source file to a different language.
Conversion options when adding a file in Water

I realize this is cheating a bit, but I did try and actually got pretty close. I mostly missed the _ before each param name and the ! on types. Also, the conversion was close but not complete. You’ll need to add another Swift class to deserialize the request body. (Well, that may not be required, but it’s what I did. I added a file named Named.swift, then added a generic to the deserialization:

Water IDE with Azure Function source code in Swift

Here’s the Named.swift file I added:

public class Named {
    var name: String?
}

Building

With all these things in place you can successfully build the project!

Unfortunately, you won’t be able to run it as you would a dotnet Azure Function.

  1. You cannot run the Azure Function app from either Water of Visual Studio.
  2. You cannot launch func host start from the project root. You will need to launch from bin/output.
  3. You’ll also find that function.json was deployed to the root of bin/output. You need to add an additional element to the specify the output directory:
    <None Include="HttpExample\function.json" LinkBase="HttpExample\">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <FinalOutputFolderOverride>.\HttpExample\</FinalOutputFolderOverride>
    </None>

NOTE: according to this comment, the element to specify the output folder will change in the next version to <DestinationFolder>.\HttpExample\</DestinationFolder>

This will get you the correct output contents and should let you successfully run func host start from bin/output.

C:\Users\ryanr\Code\OSS\silverfunc\bin\output [master ≡]> func host start
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell]

                  %%%%%%
                 %%%%%%
            @   %%%%%%    @
          @@   %%%%%%      @@
       @@@    %%%%%%%%%%%    @@@
     @@      %%%%%%%%%%        @@
       @@         %%%%       @@
         @@      %%%       @@
           @@    %%      @@
                %%
                %

Azure Functions Core Tools (3.0.2630 Commit hash: beec61496e1c5de8aa4ba38d1884f7b48233a7ab)
Function Runtime Version: 3.0.13901.0
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell]
[7/27/2020 3:21:45 AM] Building host: startup suppressed: 'False', configuration suppressed: 'False', startup operation id: 'db702c1f-f267-4b9a-9a1b-ee81a048104b'
[7/27/2020 3:21:45 AM] Reading host configuration file 'C:\Users\ryanr\Code\OSS\silverfunc\bin\output\host.json'
[7/27/2020 3:21:45 AM] Host configuration file read:
[7/27/2020 3:21:45 AM] {
[7/27/2020 3:21:45 AM]   "version": "2.0"
[7/27/2020 3:21:45 AM] }
[7/27/2020 3:21:45 AM] File 'C:\Program Files (x86)\dotnet\dotnet.exe' is not found, 'dotnet' invocation will rely on the PATH environment variable.
[7/27/2020 3:21:45 AM] Loading functions metadata
[7/27/2020 3:21:45 AM] Reading functions metadata
[7/27/2020 3:21:45 AM] 1 functions found
[7/27/2020 3:21:45 AM] 1 functions loaded
[7/27/2020 3:21:45 AM] Loading extensions from C:\Users\ryanr\Code\OSS\silverfunc\bin\output\bin. BundleConfigured: False, PrecompiledFunctionApp: False, LegacyBundle: False
[7/27/2020 3:21:45 AM] File 'C:\Program Files (x86)\dotnet\dotnet.exe' is not found, 'dotnet' invocation will rely on the PATH environment variable.
[7/27/2020 3:21:46 AM] Initializing Warmup Extension.
[7/27/2020 3:21:46 AM] Initializing Host. OperationId: 'db702c1f-f267-4b9a-9a1b-ee81a048104b'.
[7/27/2020 3:21:46 AM] Host initialization: ConsecutiveErrors=0, StartupCount=1, OperationId=db702c1f-f267-4b9a-9a1b-ee81a048104b
[7/27/2020 3:21:46 AM] LoggerFilterOptions
[7/27/2020 3:21:46 AM] {
[7/27/2020 3:21:46 AM]   "MinLevel": "None",
[7/27/2020 3:21:46 AM]   "Rules": [
[7/27/2020 3:21:46 AM]     {
[7/27/2020 3:21:46 AM]       "ProviderName": null,
[7/27/2020 3:21:46 AM]       "CategoryName": null,
[7/27/2020 3:21:46 AM]       "LogLevel": null,
[7/27/2020 3:21:46 AM]       "Filter": "<AddFilter>b__0"
[7/27/2020 3:21:46 AM]     },
[7/27/2020 3:21:46 AM]     {
[7/27/2020 3:21:46 AM]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[7/27/2020 3:21:46 AM]       "CategoryName": null,
[7/27/2020 3:21:46 AM]       "LogLevel": "None",
[7/27/2020 3:21:46 AM]       "Filter": null
[7/27/2020 3:21:46 AM]     },
[7/27/2020 3:21:46 AM]     {
[7/27/2020 3:21:46 AM]       "ProviderName": "Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.SystemLoggerProvider",
[7/27/2020 3:21:46 AM]       "CategoryName": null,
[7/27/2020 3:21:46 AM]       "LogLevel": null,
[7/27/2020 3:21:46 AM]       "Filter": "<AddFilter>b__0"
[7/27/2020 3:21:46 AM]     }
[7/27/2020 3:21:46 AM]   ]
[7/27/2020 3:21:46 AM] }
[7/27/2020 3:21:46 AM] FunctionResultAggregatorOptions
[7/27/2020 3:21:46 AM] {
[7/27/2020 3:21:46 AM]   "BatchSize": 1000,
[7/27/2020 3:21:46 AM]   "FlushTimeout": "00:00:30",
[7/27/2020 3:21:46 AM]   "IsEnabled": true
[7/27/2020 3:21:46 AM] }
[7/27/2020 3:21:46 AM] SingletonOptions
[7/27/2020 3:21:46 AM] {
[7/27/2020 3:21:46 AM]   "LockPeriod": "00:00:15",
[7/27/2020 3:21:46 AM]   "ListenerLockPeriod": "00:00:15",
[7/27/2020 3:21:46 AM]   "LockAcquisitionTimeout": "10675199.02:48:05.4775807",
[7/27/2020 3:21:46 AM]   "LockAcquisitionPollingInterval": "00:00:05",
[7/27/2020 3:21:46 AM]   "ListenerLockRecoveryPollingInterval": "00:01:00"
[7/27/2020 3:21:46 AM] }
[7/27/2020 3:21:46 AM] HttpOptions
[7/27/2020 3:21:46 AM] {
[7/27/2020 3:21:46 AM]   "DynamicThrottlesEnabled": false,
[7/27/2020 3:21:46 AM]   "MaxConcurrentRequests": -1,
[7/27/2020 3:21:46 AM]   "MaxOutstandingRequests": -1,
[7/27/2020 3:21:46 AM]   "RoutePrefix": "api"
[7/27/2020 3:21:46 AM] }
[7/27/2020 3:21:46 AM] Starting JobHost
[7/27/2020 3:21:46 AM] Starting Host (HostId=desktopm63akg2-547175781, InstanceId=1a6698de-1147-4be2-893e-c9080f538890, Version=3.0.13901.0, ProcessId=17080, AppDomainId=1, InDebugMode=False, InDiagnosticMode=False, FunctionsExtensionVersion=(null))
[7/27/2020 3:21:46 AM] Loading functions metadata
[7/27/2020 3:21:46 AM] 1 functions loaded
[7/27/2020 3:21:46 AM] Generating 1 job function(s)
[7/27/2020 3:21:46 AM] Found the following functions:
[7/27/2020 3:21:46 AM] Host.Functions.HttpExample
[7/27/2020 3:21:46 AM]
[7/27/2020 3:21:46 AM] Initializing function HTTP routes
[7/27/2020 3:21:46 AM] Mapped function route 'api/HttpExample' [get,post] to 'HttpExample'
[7/27/2020 3:21:46 AM]
[7/27/2020 3:21:46 AM] Host initialized (229ms)
[7/27/2020 3:21:46 AM] Host started (245ms)
[7/27/2020 3:21:46 AM] Job host started
Hosting environment: Production
Content root path: C:\Users\ryanr\Code\OSS\silverfunc\bin\output
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.

Http Functions:

        HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

[7/27/2020 3:21:51 AM] Host lock lease acquired by instance ID '000000000000000000000000B6B01377'.

Deploying

I didn’t bother trying to deploy this direct to Azure Functions. There’s no reason it shouldn’t work, but as I’ve been doing all Docker deployments to Azure Functions for some time now, I went ahead and added a simple Dockerfile and created an Azure Function using a custom image. Here’s the contents of the Dockerfile:

FROM mcr.microsoft.com/azure-functions/dotnet:3.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true
COPY bin/output/ /home/site/wwwroot

Once I published my image, the instructions in the link above worked just as well for this image as for the other images I’ve used. Better still, it worked perfectly!

Output from Azure Function
Hello from Swift, courtesy of RemObjects Silver!

Conclusion

I was excited to find that I could use Swift to build Azure Functions. It’s also nice to have a choice of whether to use the official Swift compiler as in Saleh’s swiftfunc or the RemObjects Elements compiler. In particular, I really enjoyed learning about and using Elements. I was pleasantly surprised to find that their tools are nice to work with and better still, they have some of the best support I’ve ever experienced. Marc reached out directly and quickly through their discussion forums, fixing a few bugs he found and providing me with an update to continue experimenting. I’m interested to see how a mixed language function project might work next.

You can find the full source code for this project on GitHub.

What do you think? Have you wanted to write Swift but lacked a Mac? Interested in some other aspect of any of these topics? Leave a comment!