Gleam의 FFI를 통해 Erlang, Elixir, Rust, JavaScript와 상호운용하는 방법을 예제로 보여주며, 외부 라이브러리 호출과 자체 코드 통합까지 다룬다.
URL: https://www.jonashietala.se/blog/2024/01/11/exploring_the_gleam_ffi/
Title: Jonas Hietala: Exploring the Gleam FFI
Published: 2024년 1월 11일 (https://www.jonashietala.se/blog/2024)
Revised: 2025년 8월 11일, 커밋 e5fae62
Tagged: Elixir, Erlang, Gleam, Rust
내 두뇌는 참 호기심이 많다. 지금 나는 출장 중이고, 꼭 끝내야 할 중요한 할 일들을 처리하려고 시간을 따로 빼 두었다. 그런데 정작 그 일들에 집중하는 대신, Gleam—젊고 흥미로운 프로그래밍 언어—을 가지고 놀기 시작했다.
내 (현재) 최애 언어는 Rust와 Elixir다. 둘은 서로 매우 다르지만, Rust는 Erlang NIFs를 통해 쉽게 임베딩할 수 있어서 함께 잘 어울린다. 이는 유용한데, Elixir(정확히는 Elixir가 실행되는 Erlang VM)의 단점 중 하나가 원시 계산 성능이며, Rust가 바로 그 부분에서 탁월하기 때문이다.
Elixir의 또 다른 단점은 타입 부재다(물론 개선이 진행 중이긴 하다). 이것이 바로 나를 Gleam으로 이끈 이유인데, Gleam은 마치 Elixir 위에 Rust 타입을 얹은 듯한 느낌을 준다—환상적인 세일즈 포인트다.
젊은 언어의 큰 단점은 라이브러리가 많지 않다는 점이다. 그래서 대부분 직접 해결책을 구현해야 한다. 하지만 기존 플랫폼을 타깃으로 삼는 장점은 그들의 라이브러리를 함께 활용할 수 있다는 것이다. Gleam의 경우에는 모든 Erlang과 Elixir(혹은 컴파일 타깃에 따라 JavaScript) 라이브러리를 쓸 수 있다는 의미다. 이는 Gleam의 FFI를 통해 가능하며, 꽤 편리하다.
자, 살펴보자.
함께 따라 하려면 해당 컴파일러들을 설치해 두어야 한다. 나는 깨끗한 새 저장소에서 예제를 시작하겠다:
$ gleam new myapp
The Gleam Book에서 보듯 표준 Erlang 함수를 호출하는 것은 간단하다. @external 키워드로 외부 함수를 선언하고 평소처럼 호출하면 된다:
import gleam/io
@external(erlang, "rand", "uniform")
pub fn random_float() -> Float
pub fn main() {
io.debug(random_float())
}
이를 실행하면 rand Erlang 모듈의 uniform 함수를 호출한다:
$ gleam run
0.43487935467166317
이 방식은 표준 라이브러리에 잘 동작하지만, Hex의 다른 Erlang 라이브러리에도 접근할 수 있다. gleam.toml에 의존성을 추가하면 된다:
[dependencies]
base32 = "~> 0.1.0"
이전과 같이 선언하고 호출하자:
@external(erlang, "base32", "encode")
pub fn encode_base32(x: String) -> String
pub fn main() {
io.debug(encode_base32("superhidden"))
}
그러면 Gleam이 Erlang 의존성을 내려받고 컴파일한다:
$ gleam run
Compiling base32
===> Fetching rebar3_hex v7.0.7
===> Fetching hex_core v0.8.4
===> Fetching verl v1.1.1
===> Analyzing applications...
===> Compiling hex_core
===> Compiling verl
===> Compiling rebar3_hex
===> Analyzing applications...
===> Compiling base32
"ON2XAZLSNBUWIZDFNY======"
직접 Erlang 코드를 작성하여 호출하는 것도 매우 쉽다. gleam 컴파일러는 .erl 파일을 자동으로 컴파일하고 포함한다.
예를 들어 src/erlib.erl 파일:
-module(erlib).
-export([ping/0]).
ping() ->
io:fwrite("ping~n", []).
gleam에서 이전과 같이 함수를 선언하고 호출하자:
@external(erlang, "erlib", "ping")
pub fn ping() -> a
pub fn main() {
ping()
}
$ gleam run
ping
Erlang에서 Gleam 함수를 호출할 수도 있다. 예를 들어 src/mypong.gleam에 다음과 같은 pong 함수가 있을 때:
import gleam/io
pub fn pong() {
io.println("pong")
}
Erlang에서는 module:function()으로 Gleam 함수를 호출할 수 있다:
ping() ->
io:fwrite("ping from Erlang~n", []),
mypong:pong(). % Gleam 함수 호출
$ gleam run
ping from Erlang
pong
기존 Elixir 프로젝트에 Gleam 코드를 포함하고 싶다면, mix가 Gleam 코드와 의존성을 처리하도록 하는 방법이 MixGleam에 잘 정리되어 있다. 반대로 Gleam 프로젝트에 Elixir 코드를 포함하고 싶다면, Erlang과 마찬가지로 손쉽게 할 수 있다.
외부 Elixir 함수 선언도 동일하게 @external 키워드를 사용한다:
import gleam/io
@external(erlang, "Elixir.RandomColor", "hex")
pub fn random_color() -> String
pub fn main() {
io.println(random_color())
}
Elixir 모듈에는 Elixir 접두사가 붙는다는 점, 그리고 Elixir가 Erlang으로 컴파일되므로 여전히 외부 Erlang 코드를 호출한다는 점에 유의하자.
의존성은 Erlang과 완전히 동일하게 Hex에서 추가한다:
$ gleam add random_color
$ gleam run
#3724C9
직접 작성한 Elixir 코드를 호출하는 것도 Erlang 때와 동일하다. 소스 디렉터리에 포함하면 된다. 예: src/exlib.ex
defmodule Exlib do
def ping() do
IO.puts("ping from Elixir")
:mypong.pong()
end
end
그리고 모듈을 Elixir.Exlib로 참조한다:
@external(erlang, "Elixir.Exlib", "ping")
pub fn ping() -> a
pub fn main() {
ping()
}
$ gleam run
ping from Elixir
pong
유의할 점:
더 가벼운 구성을 원한다면 Erlang이 더 나은 선택일 수 있다.
처음에 Elixir에서 rustler를 통해 Rust 코드를 쉽게 호출할 수 있다고 썼다. Gleam에서도 Erlang 또는 Elixir를 통해 Rust를 호출할 수 있다. Rustler는 Elixir의 mix 사용을 권장하지만, 여기서는 Erlang을 통해 작업하여 Gleam 툴체인을 계속 사용할 수 있다.
먼저 어딘가에 Rust 프로젝트를 만들어야 한다. Gleam 저장소 최상위나 native/ 폴더 등 어디든 가능하다. 이 예제에서는 최상위에 만들겠다:
$ cargo new rslib --lib
Rust 쪽에서는 rustler를 사용해 NIF를 만든다:
$ cd rslib/
$ cargo add rustler
그리고 rslib/src/lib.rs에서 노출할 함수에 rustler 어트리뷰트를 붙인다:
#[rustler::nif]
pub fn truly_random() -> i64 {
4 // 주사위 굴려서 고른 값. 무작위 보장.
}
rustler::init!("librs", [truly_random]);
동적 라이브러리로 빌드해야 하므로 Cargo.toml을 수정한다:
[lib]
crate-type = ["dylib"]
그리고 릴리스 모드로 빌드한다:
$ cargo build --release
...
그러면 target/release/librslib.so가 생성된다. 네이밍이 별로지만 그냥 진행하자.
Gleam에서 이 파일을 사용하려면 이를 priv/(Rust 프로젝트가 아닌 Gleam 루트 기준)로 복사해야 한다:
$ mkdir ../priv
$ cp target/release/librslib.so ../priv/
파일 구조는 다음과 비슷할 것이다:
├── README.md
├── build
├── gleam.toml
├── manifest.toml
├── priv
│ └── librslib.so # 중요한 부분
├── rslib
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── src
│ │ └── lib.rs
│ └── target
└── src
└── myapp.gleam
라이브러리를 포함하기 위해 약간의 Erlang을 사용하자. 앞서와 비슷한 src/rslib.erl을 만들되, Erlang NIFs를 사용한다:
-module(librs).
-export([truly_random/0]).
-nifs([truly_random/0]).
-on_load(init/0).
init() ->
ok = erlang:load_nif("priv/librslib", 0).
truly_random() ->
exit(nif_library_not_loaded).
이제 librslib 라이브러리를 로드하고, -nifs(..)로 truly_random을 NIF로 선언하며, 라이브러리가 로드되면 대체될 플레이스홀더 함수를 추가했다.
Erlang 코드가 준비되었으니 남은 일은 Gleam에서 Erlang 함수를 호출하는 것이다:
import gleam/io
@external(erlang, "librs", "truly_random")
pub fn truly_random() -> String
pub fn main() {
io.debug(truly_random())
}
이제 진정으로 무작위인 Rust 함수를 호출할 수 있다!
$ gleam run
4
Rust는 훌륭하고, Gleam에서 이를 호출할 수 있다는 점도 멋지지만, 유의해야 할 단점들이 있다:
priv/로의 이동은 Gleam 툴체인이 처리하지 않는다. 일부는 Makefile로 라이브러리를 빌드하고 priv/에 복사한다.Rust에서 Gleam 코드를 호출하는 것은 더 어려워 보인다. Rust에서 Elixir나 Erlang 함수를 호출하는 예시를 하나 찾았지만, 더 자세히 살펴보지는 않았다.
Gleam은 JavaScript로도 컴파일할 수 있으니, 짧은 예시 하나 정도는 빼놓을 수 없다.
JavaScript로 컴파일하려면 gleam.toml에 target = javascript를 추가하거나, 타깃 플래그를 사용하자:
$ gleam run --target javascript
이전의 Erlang/Elixir 때와 마찬가지로, src/에 JavaScript 소스 파일을 추가하면 자동으로 포함된다. 예: src/jslib.mjs
import * as gleam from "./mypong.mjs";
export function ping() {
console.log("ping from JavaScript");
gleam.pong();
}
외부 함수를 선언할 때는, 이제 라이브러리 이름 대신 상대 파일 경로를 사용한다:
@external(javascript, "./jslib.mjs", "ping")
pub fn ping() -> a
pub fn main() {
ping()
}
실행해 보면 JavaScript의 ping 함수를 호출하고, 그 함수가 다시 Gleam의 pong 함수를 호출함을 확인할 수 있다:
$ gleam run --target javascript
ping from JavaScript
pong
생성된 JavaScript 출력은 build/dev/javascript/myapp/에서 확인할 수 있다. 이는 디버깅하거나 Gleam이 생성하는 코드를 이해할 때 매우 유용하다.
이 글이 Gleam의 FFI 시스템을 시작하는 데 도움이 되었기를 바란다. Gleam을 며칠밖에 살펴보지 않았지만, 내가 좋아하는 다른 언어들과 그 생태계에 이렇게 쉽게 접근할 수 있다는 점 덕분에, Gleam으로 실제 무언가를 만들 때도 핵심 기능을 놓칠지 걱정할 필요는 없을 것 같다.