Skip to content

Commit 78cb4c7

Browse files
committed
Initial commit
0 parents  commit 78cb4c7

File tree

9 files changed

+273
-0
lines changed

9 files changed

+273
-0
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: erlef/setup-beam@v1
14+
with:
15+
otp-version: '28'
16+
gleam-version: '1.13.0'
17+
rebar3-version: '3'
18+
# elixir-version: "1"
19+
- run: gleam deps download
20+
- run: gleam test
21+
- run: gleam format --check src test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.beam
2+
*.ez
3+
/build
4+
erl_crash.dump

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Alistair Smith
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# gleam_manifold
2+
3+
Gleam bindings to [Manifold](https://github.com/discord/manifold) - an Elixir library for fast message passing between BEAM nodes.
4+
5+
## What is Manifold?
6+
7+
Manifold is an Elixir library originally developed by Discord that optimizes sending the same message to many processes. Instead of sending messages sequentially (which can be slow with thousands of processes), Manifold uses a divide-and-conquer approach that distributes the work across multiple sender processes, achieving much better performance at scale.
8+
9+
## Installation
10+
11+
This library is intentionally not published to hex, because it depends on internal implementation details of Gleam's `erlang/process` module. If Gleam opens up the Subject type, we can implement this library in a more official way and publish to hex.
12+
13+
For now, please depend on it as a git dependency in your `gleam.toml`:
14+
15+
```toml
16+
[dependencies]
17+
gleam_manifold = { git = "[email protected]:otters/gleam_manifold.git", ref = "<commit hash>" }
18+
```
19+
20+
## Usage
21+
22+
### Type-safe sending to Subjects
23+
24+
The primary way to use this library is with Gleam's type-safe `Subject` system:
25+
26+
```gleam
27+
import gleam/erlang/process
28+
import gleam_manifold as manifold
29+
30+
pub fn example() {
31+
let subject = process.new_subject()
32+
33+
// Send a message through Manifold
34+
manifold.send(subject, "Hello world")
35+
}
36+
```
37+
38+
### Sending to PIDs
39+
40+
For cases where you need to send to raw PIDs (not type-safe):
41+
42+
```gleam
43+
import gleam_manifold as manifold
44+
45+
pub fn send_to_pid(pid: process.Pid) {
46+
manifold.send_pid(pid, "Hello")
47+
}
48+
```
49+
50+
### Multicasting to multiple PIDs
51+
52+
Send the same message to multiple processes at once:
53+
54+
```gleam
55+
import gleam_manifold as manifold
56+
57+
pub fn broadcast_join(pids: List(process.Pid), name: String) {
58+
manifold.send_multi_pid(pids, "Hello, " <> name)
59+
}
60+
```
61+
62+
## Implementation Notes
63+
64+
### Internals Hack
65+
66+
This library uses internals of Gleam's `erlang/process` implementation by unwrapping the opaque `Subject` type in Erlang. This is technically a hack since `Subject` is meant to be opaque, but it's necessary to extract the PID and reference tag needed for sending to Gleam's Subject type. The implementation pattern matches on the internal `{subject, Pid, Ref}` tuple structure in `gleam_manifold_ffi.erl`.
67+
68+
### Current Limitations
69+
70+
1. **No options parameter support**: The Elixir Manifold library supports an options parameter for tuning performance characteristics (like partition size). This Gleam wrapper doesn't currently expose these options, though this could be added in the future.
71+
72+
2. **No named subjects support**: Named subjects (registered process names) are not currently supported. The library will fail with an assertion error if you try to use a named subject. This is fixable but hasn't been implemented yet.
73+
74+
## Testing
75+
76+
Run the tests with:
77+
78+
```bash
79+
gleam test
80+
```
81+
82+
## License
83+
84+
See the LICENSE file in the repository.

gleam.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name = "gleam_manifold"
2+
version = "1.0.0"
3+
4+
description = "Gleam bindings to github.com/discord/manifold"
5+
licences = ["MIT"]
6+
repository = { type = "github", user = "otters", repo = "gleam_manifold" }
7+
links = [{ title = "Website", href = "https://otters.app" }]
8+
9+
[dependencies]
10+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
11+
gleam_erlang = ">= 1.3.0 and < 2.0.0"
12+
manifold = ">= 1.6.0 and < 2.0.0"
13+
14+
[dev-dependencies]
15+
gleeunit = ">= 1.0.0 and < 2.0.0"

manifest.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This file was generated by Gleam
2+
# You typically do not need to edit this file
3+
4+
packages = [
5+
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
6+
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
7+
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
8+
{ name = "manifold", version = "1.6.0", build_tools = ["mix"], requirements = [], otp_app = "manifold", source = "hex", outer_checksum = "F4028A840B903447E4A0A622E6EF64A755D4B9910EE05830CA40949C6568AC87" },
9+
]
10+
11+
[requirements]
12+
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
13+
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
14+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
15+
manifold = { version = ">= 1.6.0 and < 2.0.0" }

src/gleam_manifold.gleam

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import gleam/erlang/process
2+
import gleam/erlang/reference
3+
4+
pub fn send(subject: process.Subject(message), message: message) -> Nil {
5+
let assert Ok(#(pid, ref)) = unwrap_subject(subject)
6+
as "Named subjects are currently unsupported in gleam_manifold.send()"
7+
manifold_send_subject(pid, #(ref, message))
8+
Nil
9+
}
10+
11+
/// You almost certainly do not want to use this.
12+
/// Absolutely prefer to use the type-safe variant of
13+
/// this function `manifold.send(subject, message)`
14+
pub fn send_pid(pid: process.Pid, message: message) -> Nil {
15+
manifold_send_raw(pid, message)
16+
Nil
17+
}
18+
19+
/// Call Manifold.send with a list of pids as the first argument, as
20+
/// is supported in Manifold
21+
///
22+
/// Sending multi does not support sending to subjects, because
23+
/// subjects themselves EACH have a unique tag along with them. The
24+
/// subject expects the tag to be included in the message itself, so
25+
/// this would mean that all subjects need to have the same tag, which is
26+
/// impossible because they are unique. You would have to use something
27+
/// like `process.select_other` to take advantage of receiving values
28+
/// with this function still within Gleam.
29+
pub fn send_multi_pid(pids: List(process.Pid), message: message) -> Nil {
30+
manifold_send_multi(pids, message)
31+
Nil
32+
}
33+
34+
type DoNotLeak
35+
36+
@external(erlang, "gleam_manifold_ffi", "unwrap_subject")
37+
fn unwrap_subject(
38+
subject: process.Subject(message),
39+
) -> Result(#(process.Pid, reference.Reference), Nil)
40+
41+
@external(erlang, "Elixir.Manifold", "send")
42+
fn manifold_send_subject(
43+
pid: process.Pid,
44+
message: #(reference.Reference, message),
45+
) -> DoNotLeak
46+
47+
@external(erlang, "Elixir.Manifold", "send")
48+
fn manifold_send_raw(pid: process.Pid, message: message) -> DoNotLeak
49+
50+
@external(erlang, "Elixir.Manifold", "send")
51+
fn manifold_send_multi(pid: List(process.Pid), message: message) -> DoNotLeak

src/gleam_manifold_ffi.erl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-module(gleam_manifold_ffi).
2+
-export([unwrap_subject/1]).
3+
4+
unwrap_subject({subject, Pid, Ref}) ->
5+
{ok, {Pid, Ref}};
6+
unwrap_subject(_Subject) ->
7+
{error, nil}.

test/gleam_manifold_test.gleam

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import gleam/erlang/process
2+
import gleam_manifold as manifold
3+
import gleeunit
4+
5+
pub fn main() -> Nil {
6+
gleeunit.main()
7+
}
8+
9+
pub fn simple_test() {
10+
let subject = process.new_subject()
11+
process.spawn(fn() { manifold.send(subject, "Hello world") })
12+
assert process.receive(subject, 100) == Ok("Hello world")
13+
}
14+
15+
pub fn many_pids_test() {
16+
let subject = process.new_subject()
17+
18+
let proc = fn() {
19+
let selector = process.new_selector() |> process.select_other(identity)
20+
let assert Ok(result) = process.selector_receive(selector, 5)
21+
process.send(subject, result)
22+
}
23+
24+
let a = process.spawn(proc)
25+
let b = process.spawn(proc)
26+
27+
manifold.send_pid(a, "hello from a")
28+
manifold.send_pid(b, "hello from b")
29+
30+
assert process.receive(subject, 5) == Ok("hello from a")
31+
assert process.receive(subject, 5) == Ok("hello from b")
32+
assert process.receive(subject, 5) == Error(Nil)
33+
}
34+
35+
pub fn multi_pids_test() {
36+
let subject = process.new_subject()
37+
38+
let proc = fn() {
39+
let selector = process.new_selector() |> process.select_other(identity)
40+
let assert Ok(result) = process.selector_receive(selector, 5)
41+
process.send(subject, result)
42+
}
43+
44+
let a = process.spawn(proc)
45+
let b = process.spawn(proc)
46+
47+
manifold.send_multi_pid([a, b], "hello from both")
48+
49+
assert process.receive(subject, 5) == Ok("hello from both")
50+
assert process.receive(subject, 5) == Ok("hello from both")
51+
assert process.receive(subject, 5) == Error(Nil)
52+
}
53+
54+
@external(erlang, "gleam_erlang_ffi", "identity")
55+
fn identity(x: a) -> b

0 commit comments

Comments
 (0)