Skip to content

Commit 7a86d39

Browse files
authored
Merge pull request #6 from etcwilde/ewilde/swift-macros
Adding an example of how to use Swift macros in a CMake project.
2 parents 516f1f2 + 3987bac commit 7a86d39

File tree

6 files changed

+181
-0
lines changed

6 files changed

+181
-0
lines changed

4_swift_macros/CMakeLists.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This source file is part of the Swift open source project
2+
#
3+
# Copyright (c) 2024 Apple Inc. and the Swift project authors.
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
8+
cmake_minimum_required(VERSION 3.22)
9+
10+
if(POLICY CMP0157)
11+
cmake_policy(SET CMP0157 NEW)
12+
endif()
13+
14+
project(StringifyMacroExample
15+
LANGUAGES Swift)
16+
17+
include(ExternalProject)
18+
19+
# Build the macros
20+
#
21+
# Macros must run on the same machine doing the compiling, not the one that
22+
# CMake is configured to build for. In order to support cross-compiling, we use
23+
# ExternalProject to invoke CMake again. This project assumes that the compiler
24+
# in toolchain is configured to compile for the builder by default. Otherwise,
25+
# you will need to set the `CMAKE_Swift_COMPILER_TARGET` to the builder triple.
26+
ExternalProject_Add(StringifyMacro
27+
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/StringifyMacro"
28+
INSTALL_COMMAND "")
29+
ExternalProject_Get_Property(StringifyMacro BINARY_DIR)
30+
if(CMAKE_HOST_WIN32)
31+
set(StringifyMacroPath "${BINARY_DIR}/StringifyMacro.exe#StringifyMacro")
32+
else()
33+
set(StringifyMacroPath "${BINARY_DIR}/StringifyMacro#StringifyMacro")
34+
endif()
35+
36+
add_executable(HelloMacros main.swift)
37+
add_dependencies(HelloMacros StringifyMacro)
38+
target_compile_options(HelloMacros PRIVATE -load-plugin-executable "${StringifyMacroPath}")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This source file is part of the Swift open source project
2+
#
3+
# Copyright (c) 2024 Apple Inc. and the Swift project authors.
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
8+
cmake_minimum_required(VERSION 3.22)
9+
if(POLICY CMP0157)
10+
cmake_policy(SET CMP0157 NEW)
11+
endif()
12+
13+
project(StringifyMacro
14+
LANGUAGES Swift)
15+
16+
# We use FetchContent to download and build SwiftSyntax. Unlike ExternalProject,
17+
# FetchContent does not re-invoke CMake, and the build graphs are merged. This
18+
# exposes the variables and build targets in SwiftSyntax to our Macro project.
19+
# Everything in this project is isolated, so you can have macros that require
20+
# different versions of SwiftSyntax. They won't be able to link eachother, but
21+
# will work together as imported by the compiler.
22+
include(FetchContent)
23+
FetchContent_Declare(SwiftSyntax
24+
GIT_REPOSITORY https://github.com/apple/swift-syntax.git
25+
GIT_TAG 84a4bedfd1294f6ee18e6dc9ad70df55fa6230f6)
26+
FetchContent_MakeAvailable(SwiftSyntax)
27+
28+
add_executable(StringifyMacro macro.swift)
29+
target_compile_options(StringifyMacro PRIVATE -parse-as-library)
30+
target_link_libraries(StringifyMacro
31+
SwiftSyntax
32+
SwiftSyntaxMacros
33+
SwiftCompilerPlugin)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxMacros
3+
import SwiftCompilerPlugin
4+
5+
public struct StringifyMacro: ExpressionMacro {
6+
public static func expansion(
7+
of node: some FreestandingMacroExpansionSyntax,
8+
in context: some MacroExpansionContext
9+
) throws -> ExprSyntax {
10+
guard let arg = node.arguments.first,
11+
node.arguments.count == 1 else {
12+
throw CustomError("stringify requires exactly one argument")
13+
}
14+
return "(\(arg), \(literal: arg.description))"
15+
}
16+
}
17+
18+
@main
19+
struct MacroPlugin: CompilerPlugin {
20+
let providingMacros: [Macro.Type] = [
21+
StringifyMacro.self,
22+
]
23+
}
24+
25+
struct CustomError: Error, CustomStringConvertible {
26+
var msg: String
27+
init(_ msg: String) { self.msg = msg }
28+
var description: String { self.msg }
29+
}

4_swift_macros/main.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@freestanding(expression)
2+
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "StringifyMacro", type: "StringifyMacro")
3+
4+
print(#stringify(1 + 3))

4_swift_macros/readme.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Stringify Macro Example
2+
3+
This project demonstrates building and using a Swift macro in CMake.
4+
5+
## Requirements
6+
- Swift 5.9 (5.9.0+ macOS, 5.9.1 Windows and Linux)
7+
8+
## Build Instructions
9+
10+
```sh
11+
> mkdir build && cd build
12+
> cmake -G 'Ninja' ../
13+
> ninja
14+
> ./HelloMacros
15+
(4, "1 + 3")
16+
```
17+
18+
## Description
19+
20+
Macros must build for the local machine doing the building, not the machine
21+
you're building for because the compiler executes them. It's important to
22+
consider the macro as a separate project from the main part of the codebase if
23+
there is any chance you will want to cross-compile the project.
24+
25+
We can manually invoke CMake twice, once to build the macro, then again to build
26+
the actual project for whatever platform we're building for, but that isn't very
27+
convenient. Instead, we use `ExternalProject_Add` to include the macro project
28+
which invokes compile
29+
30+
```cmake
31+
ExternalProject_Add(StringifyMacro
32+
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/StringifyMacro"
33+
INSTALL_COMMAND "")
34+
ExternalProject_Get_Property(StringifyMacro BINARY_DIR)
35+
string(StringifyMacroPath "${BINARY_DIR}/StringifyMacro#StringifyMacro")
36+
```
37+
38+
Technically, neither the compiler nor CMake know the triple for the build
39+
machine. This is normal, but inconvenient. We are assuming that the Swift
40+
compiler is configured to compile for the build machine by default.
41+
`ExternalProject_Add` doesn't forward variables from the current project into
42+
the external project, which is normally a pain-point of external projects, but
43+
in this case, is exactly what we want.
44+
45+
In the macro project, we need to import SwiftSyntax to access the macro
46+
libraries. Since SwiftSyntax and the macro are built to run in the same
47+
environment, we can use `FetchContent` to merge the build graphs and build the
48+
entire macro project as one.
49+
```cmake
50+
FetchContent_Declare(SwiftSyntax
51+
GIT_REPOSITORY https://github.com/apple/swift-syntax.git
52+
GIT_TAG 247e3ce379141f81d56e067fff5ff13135bd5810)
53+
FetchContent_MakeAvailable(SwiftSyntax)
54+
```
55+
56+
Then it's just a matter of linking our macro executable against the needed
57+
libraries.
58+
59+
```cmake
60+
add_executable(StringifyMacro macro.swift)
61+
target_compile_options(StringifyMacro PRIVATE -parse-as-library)
62+
target_link_libraries(StringifyMacro
63+
SwiftSyntax
64+
SwiftSyntaxMacros
65+
SwiftCompilerPlugin)
66+
```

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,14 @@ Requires:
4646
- CMake 3.26
4747
- Swift 5.9
4848
- Clang 11 or Apple Clang in Xcode 12 or newer
49+
50+
## Swift Macros
51+
52+
Directory: `4_swift_macros`
53+
54+
This project demonstrates how to build a custom macro in a CMake-based project
55+
using the Swift macro support introduced in Swift 5.9.
56+
57+
Requires:
58+
59+
- Swift 5.9 (macOS: Swift 5.9.0, Windows and Linux: Swift 5.9.1)

0 commit comments

Comments
 (0)