Skip to content

Commit

Permalink
Implement a native way to define module initialization (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
SavchenkoValeriy authored Aug 21, 2022
1 parent b1ed8ca commit caa637e
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 38 deletions.
26 changes: 22 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.2
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -9,11 +9,17 @@ let package = Package(
products: [
.library(
name: "EmacsSwiftModule",
targets: ["EmacsSwiftModule"]),
targets: ["EmacsSwiftModule"]
),
.plugin(
name: "ModuleFactoryPlugin",
targets: ["ModuleFactoryPlugin"]
),
.library(
name: "TestModule",
type: .dynamic,
targets: ["TestModule"]),
targets: ["TestModule"]
),
],
dependencies: [],
targets: [
Expand All @@ -28,10 +34,22 @@ let package = Package(
path: "Source/C",
publicHeadersPath: "include"
),
.plugin(
name: "ModuleFactoryPlugin",
capability: .buildTool(),
dependencies: [.target(name: "ModuleInitializerInjector")]
),
.executableTarget(
name: "ModuleInitializerInjector",
path: "Plugins",
exclude: ["ModuleFactoryPlugin/ModuleFactoryPlugin.swift"],
sources: ["ModuleInitializerInjector.swift"]
),
.target(
name: "TestModule",
dependencies: ["EmacsSwiftModule"],
path: "Test/TestModule"
path: "Test/TestModule",
plugins: ["ModuleFactoryPlugin"]
),
]
)
Expand Down
16 changes: 16 additions & 0 deletions Plugins/ModuleFactoryPlugin/ModuleFactoryPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import PackagePlugin

@main
struct ModuleFactoryPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
let outputPath = context.pluginWorkDirectory.appending("ModuleInitializer.swift")
return [
.buildCommand(
displayName: "Module initialization injection",
executable: try context.tool(named: "ModuleInitializerInjector").path,
arguments: [outputPath.string],
outputFiles: [outputPath]
)
]
}
}
36 changes: 36 additions & 0 deletions Plugins/ModuleInitializerInjector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

@main
public struct ModuleInitializerInjector {
static func main() async throws {
guard CommandLine.argc == 2 else {
print("Injector takes one argument")
return
}

let initializerSourcePath = URL(fileURLWithPath: CommandLine.arguments[1])
let initializerSource = """
import EmacsSwiftModule
@_cdecl("plugin_is_GPL_compatible")
public func isGPLCompatible() {}
@_cdecl("emacs_module_init")
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
do {
let module: Module = createModule()
if !module.isGPLCompatible {
print("Emacs dynamic modules have to be distributed under a GPL compatible license!")
return 1
}
let env = Environment(from: runtimePtr)
try module.Init(env)
} catch {
return 1
}
return 0
}
"""
try initializerSource.write(to: initializerSourcePath, atomically: true, encoding: .utf8)
}
}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# 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)
# 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)

A Swift library to write Emacs plugins in Swift!

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

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

Or add `"https://github.com/SavchenkoValeriy/emacs-swift-module.git"` directly via Xcode.
Expand Down
49 changes: 27 additions & 22 deletions Source/Swift/Documentation.docc/DefiningAModule.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Defining a new Emacs module from Swift.
Add the following line to you package dependencies:

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

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

```swift
products: [
.library(
name: "AwesomeSwiftEmacsModule",
type: .dynamic,
targets: ["AwesomeSwiftEmacsModule"]),
]
.library(
name: "AwesomeSwiftEmacsModule",
type: .dynamic,
targets: ["AwesomeSwiftEmacsModule"]),
],
dependencies: [
.package("https://github.com/SavchenkoValeriy/emacs-swift-module.git", from: "1.3.0")
],
targets: [
.target(
name: "AwesomeSwiftEmacsModule",
dependencies: ["EmacsSwiftModule"],
plugins: ["ModuleFactoryPlugin"]
)
]
```

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

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.
## Writing a module code

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

```swift
import EmacsSwiftModule

@_cdecl("plugin_is_GPL_compatible")
public func isGPLCompatible() {}

@_cdecl("emacs_module_init")
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
let env = Environment(from: runtimePtr)
do {
class AwesomeSwiftEmacsModule: Module {
let isGPLCompatible = true
func Init(_ env: Environment) throws {
// initialize your module here
try env.funcall("message", "Hello from Swift!")
} catch {
return 1
}
return 0
}
```

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.
func createModule() -> Module { AwesomeSwiftEmacsModule() }
```

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

> 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!
> Important: Uncaught exceptions in the `Init` method prevent your module from loading, use that only when you absolutely cannot proceed.
13 changes: 13 additions & 0 deletions Source/Swift/Module.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Emacs dynamic module, @main class of your package.
public protocol Module {
/// Every dynamic module should be distributed under the GPL compatible license.
///
/// By returning true here, you agree that your module follows this rule.
var isGPLCompatible: Bool { get }
/// Module initialization point.
///
/// 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.
///
/// When this function finishes its execution, the given environment becomes invalid and shouldn't be used. See <doc:Lifetimes> for more details.
func Init(_ env: Environment) throws
}
16 changes: 6 additions & 10 deletions Test/TestModule/Test.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import EmacsSwiftModule

@_cdecl("plugin_is_GPL_compatible")
public func isGPLCompatible() {}

struct MyError: Error {
let x: Int
}
Expand All @@ -26,10 +23,10 @@ func someAsyncTaskWithResult(completion: (Int) -> Void) async throws {
completion(42)
}

@_cdecl("emacs_module_init")
public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
let env = Environment(from: runtimePtr)
do {
class TestModule: Module {
let isGPLCompatible = true

func Init(_ env: Environment) throws {
try env.defun("swift-int") { (arg: Int) in arg * 2 }
try env.defun("swift-float") { (arg: Double) in arg * 2 }
try env.defun("swift-bool") { (arg: Bool) in !arg }
Expand Down Expand Up @@ -184,8 +181,7 @@ public func Init(_ runtimePtr: RuntimePointer) -> Int32 {
}
}
}
} catch {
return 1
}
return 0
}

func createModule() -> Module { TestModule() }

0 comments on commit caa637e

Please # to comment.