Skip to content

Commit 811fa15

Browse files
committed
initial version of a custom Swift Runtime for AWS Lambda
1 parent 261a6df commit 811fa15

File tree

14 files changed

+205
-68
lines changed

14 files changed

+205
-68
lines changed

.gitignore

+3-68
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,3 @@
1-
# Xcode
2-
#
3-
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4-
5-
## Build generated
6-
build/
7-
DerivedData/
8-
9-
## Various settings
10-
*.pbxuser
11-
!default.pbxuser
12-
*.mode1v3
13-
!default.mode1v3
14-
*.mode2v3
15-
!default.mode2v3
16-
*.perspectivev3
17-
!default.perspectivev3
18-
xcuserdata/
19-
20-
## Other
21-
*.moved-aside
22-
*.xccheckout
23-
*.xcscmblueprint
24-
25-
## Obj-C/Swift specific
26-
*.hmap
27-
*.ipa
28-
*.dSYM.zip
29-
*.dSYM
30-
31-
## Playgrounds
32-
timeline.xctimeline
33-
playground.xcworkspace
34-
35-
# Swift Package Manager
36-
#
37-
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38-
# Packages/
39-
# Package.pins
40-
# Package.resolved
41-
.build/
42-
43-
# CocoaPods
44-
#
45-
# We recommend against adding the Pods directory to your .gitignore. However
46-
# you should judge for yourself, the pros and cons are mentioned at:
47-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48-
#
49-
# Pods/
50-
51-
# Carthage
52-
#
53-
# Add this line if you want to avoid checking in source code from Carthage dependencies.
54-
# Carthage/Checkouts
55-
56-
Carthage/Build
57-
58-
# fastlane
59-
#
60-
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61-
# screenshots whenever they are needed.
62-
# For more information about the recommended setup visit:
63-
# https://docs.fastlane.tools/best-practices/source-control/#source-control
64-
65-
fastlane/report.xml
66-
fastlane/Preview.html
67-
fastlane/screenshots/**/*.png
68-
fastlane/test_output
1+
.DS_Store
2+
.build_linux
3+
lambda.zip

AWSLambdaSwift/.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

AWSLambdaSwift/Package.swift

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version:4.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "AWSLambdaSwift",
7+
products: [
8+
.library(
9+
name: "AWSLambdaSwift",
10+
targets: ["AWSLambdaSwift"]),
11+
],
12+
dependencies: [],
13+
targets: [
14+
.target(
15+
name: "AWSLambdaSwift",
16+
dependencies: []),
17+
]
18+
)

AWSLambdaSwift/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# AWSLambdaSwift
2+
3+
A description of this package.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import Foundation
2+
3+
func log(_ object: Any, flush: Bool = false) {
4+
fputs("\(object)\n", stderr)
5+
if flush {
6+
fflush(stderr)
7+
}
8+
}
9+
10+
public typealias JSONDictionary = [String: Any]
11+
12+
public class Runtime {
13+
var counter = 0
14+
let urlSession: URLSession
15+
let awsLambdaRuntimeAPI: String
16+
let handler: String
17+
var lambdas: [String: (JSONDictionary) -> JSONDictionary]
18+
19+
public init() throws {
20+
self.urlSession = URLSession.shared
21+
self.lambdas = [:]
22+
23+
let environment = ProcessInfo.processInfo.environment
24+
guard let awsLambdaRuntimeAPI = environment["AWS_LAMBDA_RUNTIME_API"],
25+
let handler = environment["_HANDLER"] else {
26+
throw RuntimeError.missingEnvironmentVariables
27+
}
28+
29+
self.awsLambdaRuntimeAPI = awsLambdaRuntimeAPI
30+
self.handler = handler
31+
}
32+
33+
func getNextInvocation() throws -> (input: JSONDictionary, requestId: String) {
34+
let getNextInvocationEndpoint = URL(string: "http://\(awsLambdaRuntimeAPI)/2018-06-01/runtime/invocation/next")!
35+
let (optData, optResponse, optError) = urlSession.synchronousDataTask(with: getNextInvocationEndpoint)
36+
37+
guard optError == nil else {
38+
throw RuntimeError.endpointError(optError!.localizedDescription)
39+
}
40+
41+
guard let data = optData else {
42+
throw RuntimeError.missingData
43+
}
44+
45+
guard let jsonObject = try? JSONSerialization.jsonObject(with: data),
46+
let input = jsonObject as? JSONDictionary else {
47+
throw RuntimeError.invalidData
48+
}
49+
50+
let httpResponse = optResponse as! HTTPURLResponse
51+
let requestId = httpResponse.allHeaderFields["Lambda-Runtime-Aws-Request-Id"] as! String
52+
return (input: input, requestId: requestId)
53+
}
54+
55+
func postInvocationResponse(for requestId: String, response: JSONDictionary) throws {
56+
let postInvocationResponseEndpoint = URL(string: "http://\(awsLambdaRuntimeAPI)/2018-06-01/runtime/invocation/\(requestId)/response")!
57+
guard let httpBody = try? JSONSerialization.data(withJSONObject: response) else {
58+
throw RuntimeError.invalidData
59+
}
60+
61+
var urlRequest = URLRequest(url: postInvocationResponseEndpoint)
62+
urlRequest.httpMethod = "POST"
63+
urlRequest.httpBody = httpBody
64+
65+
_ = urlSession.synchronousDataTask(with: urlRequest)
66+
}
67+
68+
public func registerLambda(_ name: String, handler: @escaping (JSONDictionary) -> JSONDictionary) {
69+
lambdas[name] = handler
70+
}
71+
72+
public func start() throws {
73+
while true {
74+
let (input, requestId) = try getNextInvocation()
75+
counter += 1
76+
log("Invocation-Counter: \(counter)")
77+
78+
let lambda = lambdas["lambda"]!
79+
let output = lambda(input)
80+
try postInvocationResponse(for: requestId, response: output)
81+
}
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
enum RuntimeError: Error {
2+
case missingEnvironmentVariables
3+
case endpointError(String)
4+
case missingData
5+
case invalidData
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
extension URLSession {
4+
func synchronousDataTask(with url: URL) -> (Data?, URLResponse?, Error?) {
5+
let urlRequest = URLRequest(url: url)
6+
return synchronousDataTask(with: urlRequest)
7+
}
8+
9+
func synchronousDataTask(with urlRequest: URLRequest) -> (Data?, URLResponse?, Error?) {
10+
var data: Data?
11+
var response: URLResponse?
12+
var error: Error?
13+
14+
let semaphore = DispatchSemaphore(value: 0)
15+
16+
let dataTask = self.dataTask(with: urlRequest) {
17+
data = $0
18+
response = $1
19+
error = $2
20+
21+
semaphore.signal()
22+
}
23+
dataTask.resume()
24+
25+
_ = semaphore.wait(timeout: .distantFuture)
26+
return (data, response, error)
27+
}
28+
}

ExampleLambda/.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

ExampleLambda/Package.resolved

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"object": {
3+
"pins": [
4+
5+
]
6+
},
7+
"version": 1
8+
}

ExampleLambda/Package.swift

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version:4.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "ExampleLambda",
7+
dependencies: [
8+
.package(path: "../AWSLambdaSwift")
9+
],
10+
targets: [
11+
.target(
12+
name: "ExampleLambda",
13+
dependencies: ["AWSLambdaSwift"]),
14+
]
15+
)

ExampleLambda/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ExampleLambda
2+
3+
A description of this package.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import AWSLambdaSwift
2+
3+
func lambda(input: JSONDictionary) -> JSONDictionary {
4+
print(input)
5+
return ["hello": "world", "number": 42, "strings": ["one", "two", "three"]]
6+
}
7+
8+
let runtime = try Runtime()
9+
runtime.registerLambda("lambda", handler: lambda)
10+
try runtime.start()

Makefile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
EXECUTABLE=ExampleLambda
2+
PROJECT_PATH=ExampleLambda
3+
LAMBDA_ZIP=lambda.zip
4+
5+
clean:
6+
rm $(LAMBDA_ZIP) || true
7+
rm -r $(PROJECT_PATH)/.build_linux || true
8+
9+
build:
10+
docker run \
11+
--rm \
12+
--volume "$(shell pwd)/:/src" \
13+
--workdir "/src/$(PROJECT_PATH)" \
14+
swift \
15+
swift build --build-path ./.build_linux
16+
17+
package_lambda: clean build
18+
zip -r -j $(LAMBDA_ZIP) bootstrap $(PROJECT_PATH)/.build_linux/debug/$(EXECUTABLE)

bootstrap

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
/opt/swift-shared-libs/ld-linux-x86-64.so.2 --library-path /opt/swift-shared-libs/lib ./ExampleLambda

0 commit comments

Comments
 (0)