nseaSeb
I’ve just put together a small POC exploring PDF inspection from Elixir/Phoenix:
The idea is pretty simple: drag & drop a PDF in a Phoenix LiveView application, pass it to pdf2md from Firecrawl’s pdf-inspector, and display the extracted information (PDF type, number of pages, tables, Markdown, etc.).
The interesting part for me was not really the PDF processing itself, but how to integrate the Rust component with Elixir.
For this POC, I deliberately avoided a NIF.
Instead, the Rust binary is invoked through an OS Port:
Port.open(
{:spawn_executable, path},
[:binary, :exit_status, args: args]
)
The Rust binary is vendored in priv/rust_bin/bin/, so the Phoenix application can invoke it directly.
This feels like a rather nice boundary for this kind of workload:
-
Elixir/Phoenix remains responsible for the application and supervision.
-
Rust does the CPU-heavy/PDF-specific work.
-
No NIF means no risk of blocking or crashing the BEAM VM from native code.
-
The interface between the two sides remains very explicit.
Obviously, there are trade-offs compared with a NIF: process startup, serialization, IPC overhead, error handling, deployment, etc.
So I’m curious about how others in the Elixir ecosystem would approach this.
Would you use an OS Port for this kind of integration, or would you go for a NIF / Rustler?
This is only a POC for now, but I’m particularly interested in feedback on the architecture rather than the PDF extraction itself.
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security











Showing Posts 1 to 8- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
jc00ke
I think a port is a good option for this, without the downsides of a NIF, mainly that crashes can take down the BEAM. I recently implemented this CNode pattern, which I think is a great option for this kind of interop. In my case, instead of wrapping with Crystal, I used erl_interface directly in my Go and used zig to build the Go project as a CGo binary. I should write that up someday. I never went back to it and never got beyond the PoC “echo”, but I really like the pattern.
nseaSeb
I agree for the port, more latency than nif I guess but can’t crash the beam.
dimitarvp
Well, anecdotal evidence: I never had a Rust program crash on me.
The only times that actually happened was due to a lack of memory and the Linux OOM reaper killing them.
I do like the architecture though; ports are awesome and in this case when you have an assembly line to process documents, it’s absolutely the better pattern to use – and not super difficult to rework to distributed clusters of nodes, too.
conradwt
I like the overall architecture and thanks so much for sharing.
mogery
Hey! This is a cool project. Disclosure: I work at Firecrawl.
A note regarding crashes: We run pdf-inspector in production on every PDF we encounter via a Node.js native addon (via napi-rs), which has crash characteristics very similar to NIF. We deliberately avoid having any code to the library that can cause panics.
nseaSeb
Hello, thank you—it’s really nice to receive your comment. I have no doubt that it’s stable; in my opinion, the gain from using Nifs doesn’t matter here, but perhaps I’m mistaken?
derek-zhou
Crash isolation is only part of the considerations. The others are:
dimitarvp
That question itself made me skip shelling out to
Port-s many times in the past btw. But with time I started seeing that with a good orchestrator built on top of Oban / ObanPro you can just update state in the DB f.ex. “job for PDF fileblah_blah_173.pdfis still ongoing; this is the remote job ID (can be anything, even Fly.IO VM ID) – check it in 20 minutes and if it’s still not done, kill and retry”.Of course, orchestration itself is not always as easy but now in the age of LLMs we have multiple excellent offerings in addition to Oban / ObanPro with different tradeoffs for almost every scenario so this is even more readily available than before.
(Though quite honestly, when you have an excellent durable orchestration library that also makes the argument for working inside the BEAM again because your worst-case scenario is to orphan a PDF generation job which is far from the end of the world; you’ll try to find the orphaned file / job and just resume.)