Skip to content

Commit caa637e

Browse files
Implement a native way to define module initialization (#7)
1 parent b1ed8ca commit caa637e

File tree

7 files changed

+123
-38
lines changed

7 files changed

+123
-38
lines changed

Package.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.5
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -9,11 +9,17 @@ let package = Package(
99
products: [
1010
.library(
1111
name: "EmacsSwiftModule",
12-
targets: ["EmacsSwiftModule"]),
12+
targets: ["EmacsSwiftModule"]
13+
),
14+
.plugin(
15+
name: "ModuleFactoryPlugin",
16+
targets: ["ModuleFactoryPlugin"]
17+
),
1318
.library(
1419
name: "TestModule",
1520
type: .dynamic,
16-
targets: ["TestModule"]),
21+
targets: ["TestModule"]
22+
),
1723
],
1824
dependencies: [],
1925
targets: [
@@ -28,10 +34,22 @@ let package = Package(
2834
path: "Source/C",
2935
publicHeadersPath: "include"
3036
),
37+
.plugin(
38+
name: "ModuleFactoryPlugin",
39+
capability: .buildTool(),
40+
dependencies: [.target(name: "ModuleInitializerInjector")]
41+
),
42+
.executableTarget(
43+
name: "ModuleInitializerInjector",
44+
path: "Plugins",
45+
exclude: ["ModuleFactoryPlugin/ModuleFactoryPlugin.swift"],
46+
sources: ["ModuleInitializerInjector.swift"]
47+
),
3148
.target(
3249
name: "TestModule",
3350
dependencies: ["EmacsSwiftModule"],
34-
path: "Test/TestModule"
51+
path: "Test/TestModule",
52+
plugins: ["ModuleFactoryPlugin"]
3553
),
3654
]
3755
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct ModuleFactoryPlugin: BuildToolPlugin {
5+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
6+
let outputPath = context.pluginWorkDirectory.appending("ModuleInitializer.swift")
7+
return [
8+
.buildCommand(
9+
displayName: "Module initialization injection",
10+
executable: try context.tool(named: "ModuleInitializerInjector").path,
11+
arguments: [outputPath.string],
12+
outputFiles: [outputPath]
13+
)
14+
]
15+
}
16+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
3+
@main
4+
public struct ModuleInitializerInjector {
5+
static func main() async throws {
6+
guard CommandLine.argc == 2 else {
7+
print("Injector takes one argument")
8+
return
9+
}
10+
11+
let initializerSourcePath = URL(fileURLWithPath: CommandLine.arguments[1])
12+
let initializerSource = """
13+
import EmacsSwiftModule
14+
15+
@_cdecl("plugin_is_GPL_compatible")
16+
public func isGPLCompatible() {}
17+
18+
@_cdecl("emacs_module_init")
19+
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
20+
do {
21+
let module: Module = createModule()
22+
if !module.isGPLCompatible {
23+
print("Emacs dynamic modules have to be distributed under a GPL compatible license!")
24+
return 1
25+
}
26+
let env = Environment(from: runtimePtr)
27+
try module.Init(env)
28+
} catch {
29+
return 1
30+
}
31+
return 0
32+
}
33+
"""
34+
try initializerSource.write(to: initializerSourcePath, atomically: true, encoding: .utf8)
35+
}
36+
}

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# EmacsSwiftModule [![Emacs](https://img.shields.io/badge/Emacs-25.3%2B-blueviolet)](https://www.gnu.org/software/emacs/) [![Swift Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSavchenkoValeriy%2Femacs-swift-module%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/SavchenkoValeriy/emacs-swift-module) [![OS](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSavchenkoValeriy%2Femacs-swift-module%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/SavchenkoValeriy/emacs-swift-module) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
1+
# EmacsSwiftModule
2+
[![Emacs](https://img.shields.io/badge/Emacs-25.3%2B-blueviolet)](https://www.gnu.org/software/emacs/) [![Swift Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSavchenkoValeriy%2Femacs-swift-module%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/SavchenkoValeriy/emacs-swift-module) [![OS](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSavchenkoValeriy%2Femacs-swift-module%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/SavchenkoValeriy/emacs-swift-module) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
23

34
A Swift library to write Emacs plugins in Swift!
45

@@ -67,7 +68,7 @@ Full documentation of the package can be found here: https://savchenkovaleriy.gi
6768
Add the following line to you package dependencies:
6869

6970
```swift
70-
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.2.0")
71+
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.3.0")
7172
```
7273

7374
Or add `"https://github.com/SavchenkoValeriy/emacs-swift-module.git"` directly via Xcode.

Source/Swift/Documentation.docc/DefiningAModule.md

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Defining a new Emacs module from Swift.
99
Add the following line to you package dependencies:
1010

1111
```swift
12-
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.2.0")
12+
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.3.0")
1313
```
1414

1515
Or add `"https://github.com/SavchenkoValeriy/emacs-swift-module.git"` directly via Xcode.
@@ -20,44 +20,49 @@ Make sure that you have a dynamic library product for your module target similar
2020

2121
```swift
2222
products: [
23-
.library(
24-
name: "AwesomeSwiftEmacsModule",
25-
type: .dynamic,
26-
targets: ["AwesomeSwiftEmacsModule"]),
27-
]
23+
.library(
24+
name: "AwesomeSwiftEmacsModule",
25+
type: .dynamic,
26+
targets: ["AwesomeSwiftEmacsModule"]),
27+
],
28+
dependencies: [
29+
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.3.0")
30+
],
31+
targets: [
32+
.target(
33+
name: "AwesomeSwiftEmacsModule",
34+
dependencies: ["EmacsSwiftModule"],
35+
plugins: ["ModuleFactoryPlugin"]
36+
)
37+
]
2838
```
2939

30-
## Writing a module code
40+
And the target should depend on the `ModuleFactoryPlugin` to automatically setup C definitions required for each dynamic module.
3141

32-
Each module is required to have two exported C functions: `plugin_is_GPL_compatible` and `emacs_module_init`. The first one is the way to tell Emacs that your code is GPL-compatible. The second function is your module's `main` function. It is called when Emacs loads your module.
42+
## Writing a module code
3343

34-
Of course, we can define these two functions from Swift and this is how you'd typically do this.
44+
Each module should have a class conforming to ``Module``. This protocol has only two requirements:
45+
- ``Module/isGPLCompatible``, a boolean property that should always return true telling Emacs that your code is GPL-compatible.
46+
- ``Module/Init(_:)``, your module's `main` function, it is called when Emacs loads your module.
3547

3648
```swift
3749
import EmacsSwiftModule
3850

39-
@_cdecl("plugin_is_GPL_compatible")
40-
public func isGPLCompatible() {}
41-
42-
@_cdecl("emacs_module_init")
43-
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
44-
let env = Environment(from: runtimePtr)
45-
do {
51+
class AwesomeSwiftEmacsModule: Module {
52+
let isGPLCompatible = true
53+
func Init(_ env: Environment) throws {
4654
// initialize your module here
4755
try env.funcall("message", "Hello from Swift!")
48-
} catch {
49-
return 1
5056
}
51-
return 0
5257
}
53-
```
5458

55-
This should be the only reminder of us working directly with a C interface. ``Environment`` is your entry-point into the Emacs world from Swift.
59+
func createModule() -> Module { AwesomeSwiftEmacsModule() }
60+
```
5661

5762
Now, if you compile this code with `swift build` and load it from Emacs via
5863
```emacs-lisp
5964
(module-load "SOURCE_DIR/.build/debug/libYOUR_MODULE_NAME.dylib")
6065
```
6166
you should see the `"Hello from Swift!"` message in your Emacs.
6267

63-
> Important: Don't throw exceptions in your init methods, crashing the module is the same as crashing the whole Emacs. That's not what Emacs users expect from their plugins!
68+
> Important: Uncaught exceptions in the `Init` method prevent your module from loading, use that only when you absolutely cannot proceed.

Source/Swift/Module.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// Emacs dynamic module, @main class of your package.
2+
public protocol Module {
3+
/// Every dynamic module should be distributed under the GPL compatible license.
4+
///
5+
/// By returning true here, you agree that your module follows this rule.
6+
var isGPLCompatible: Bool { get }
7+
/// Module initialization point.
8+
///
9+
/// This function gets executed only once when the user loads the module from Emacs. Usually the module defines some package-specific functions here and/or creates the channel of communication with Emacs.
10+
///
11+
/// When this function finishes its execution, the given environment becomes invalid and shouldn't be used. See <doc:Lifetimes> for more details.
12+
func Init(_ env: Environment) throws
13+
}

Test/TestModule/Test.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import EmacsSwiftModule
22

3-
@_cdecl("plugin_is_GPL_compatible")
4-
public func isGPLCompatible() {}
5-
63
struct MyError: Error {
74
let x: Int
85
}
@@ -26,10 +23,10 @@ func someAsyncTaskWithResult(completion: (Int) -> Void) async throws {
2623
completion(42)
2724
}
2825

29-
@_cdecl("emacs_module_init")
30-
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
31-
let env = Environment(from: runtimePtr)
32-
do {
26+
class TestModule: Module {
27+
let isGPLCompatible = true
28+
29+
func Init(_ env: Environment) throws {
3330
try env.defun("swift-int") { (arg: Int) in arg * 2 }
3431
try env.defun("swift-float") { (arg: Double) in arg * 2 }
3532
try env.defun("swift-bool") { (arg: Bool) in !arg }
@@ -184,8 +181,7 @@ public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
184181
}
185182
}
186183
}
187-
} catch {
188-
return 1
189184
}
190-
return 0
191185
}
186+
187+
func createModule() -> Module { TestModule() }

0 commit comments

Comments
 (0)