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.
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.
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:
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.
- You cannot run the Azure Function app from either Water of Visual Studio.
- You cannot launch
func host start
from the project root. You will need to launch frombin/output
. - You’ll also find that
function.json
was deployed to the root ofbin/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!
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!
You must be logged in to post a comment.