Named after the famed telegraph engineer, Émile provides ultra-low overhead Scala 3 Native bindings for libuv with first-class cats-effect integration.
Émile is designed as the I/O foundation for high-performance Scala Native applications.
Émile replaces cats-effect's default polling system entirely. Where cats-effect Native uses raw epoll/kqueue syscalls and scala-native-loop runs libuv alongside ExecutionContext, Émile's LibuvPollingSystem implements cats-effect's PollingSystem trait directly; making libuv THE event loop that drives fiber scheduling.
This means:
- Zero bridging overhead: libuv callbacks wake fibers directly, no
Future→IOconversion - Single event loop: All I/O (TCP, DNS, timers, signals) flows through the same loop that cats-effect uses for work-stealing
- Per-worker loops: Each cats-effect worker thread gets its own libuv loop, matching cats-effect's thread model exactly
| Aspect | Émile | scala-native-loop | cats-effect Native |
|---|---|---|---|
| Runtime integration | Replaces PollingSystem |
Separate from runtime | Built-in epoll/kqueue |
| Event loop | libuv is the poller | libuv + ExecutionContext |
Raw syscalls |
| Fiber wakeup | Direct via uv_async_t |
Via Future/Promise |
Direct via eventfd/pipe |
| Handle lifecycle | Phantom types (Open/Closed) |
Manual | Resource-based |
| Error handling | Typed channels (Eff[IO, E, A]) |
Exceptions | IO exceptions |
| DNS resolution | ✅ async via libuv | ❌ | ✅ blocking/threadpool |
| Signal handling | ✅ async-safe bridge | ❌ | ❌ |
scala-native-loop uses NativeExecutionContext and libuv's global uv_default_loop(). This requires bridging between libuv's callback model and Scala's Future.
cats-effect Native uses raw syscalls (epoll_create1/kqueue) for fd-level polling. Efficient but limited to file descriptor readiness - DNS, timers, and signals require separate mechanisms.
Émile exposes libuv's full-featured async primitives directly to cats-effect fibers through a unified PollingSystem. No bridging, separate loops, or blocking thread pools.
┌──────────────┐ ┌───────────────┐ ┌───────────────┐
│ emile-ipa │───►│ emile-core │───►│ emile-cats │
│ (addresses) │ │ (libuv FFI) │ │ (cats-effect) │
└──────────────┘ └───────────────┘ └───────────────┘
JVM/JS/Native Native Native
| Module | Platform | Description |
|---|---|---|
| emile-ipa | JVM/JS/Native | IP addresses, ports, socket addresses with compile-time literals |
| emile-core | Native | libuv bindings: Loop, Tcp, Timer, Async, Poll, Signal, Dns handles |
| emile-cats | Native | cats-effect integration with Resource, IO, Eff, DnsResolver, SignalStream |
// build.sbt
libraryDependencies ++= Seq(
"io.github.arashi01" %%% "emile-ipa" % "0.1.0", // Cross-platform
"io.github.arashi01" %%% "emile-cats" % "0.1.0" // Native only
)import cats.effect.*
import emile.*
import emile.cats.*
import emile.ipa.*
import emile.ipa.literals.*
object HelloServer extends EmileIOApp:
def run(args: List[String]): IO[ExitCode] =
EmileIOApp.withLoop { loop =>
given Loop = loop
val address = SocketAddress.v4(ipv4"0.0.0.0", port"8080")
TcpResource.bind(address).use { server =>
// Server logic here
Eff.succeed[IO, EmileError, ExitCode](ExitCode.Success)
}
}.either.flatMap {
case Right(code) => IO.pure(code)
case Left(err) => IO.println(s"Error: $err").as(ExitCode.Error)
}Zero-allocation IP and socket address types with compile-time validation.
import emile.ipa.*
import emile.ipa.literals.*
// Compile-time validated literals
val addr = ipv4"192.168.1.1"
val p = port"8080"
val v6 = ipv6"::1"
// Runtime parsing with typed errors
val parsed: Either[AddressError, SocketAddress] =
SocketAddress.from("[::1]:443")
// IPv6 with scope ID
val scoped = SocketAddress.from("[fe80::1%eth0]:8080")
// Socket address construction
val socket = SocketAddress.v4(Ipv4Address.Loopback, Port(8080))Types:
Port- TCP/UDP port (0-65535)Ipv4Address- IPv4 address as 32-bit intIpv6Address- IPv6 address as two 64-bit longsSocketAddress- V4 or V6 with port, flow info, scope ID
Direct libuv bindings with phantom-state tracking.
import emile.*
// Create and run an event loop
for
loop <- Loop.create
result <- loop.run(RunMode.Default)
_ <- loop.close
yield result
// TCP server
for
tcp <- Tcp.init(loop)
_ <- tcp.bind(address)
_ <- tcp.listen(128) { status =>
if status >= 0 then
for client <- Tcp.init(loop); _ <- tcp.accept(client)
yield client.readStart(data => println(s"Received: $data"))
}
yield tcpHandle Types:
Loop- Event loop lifecycleTcp[S]- TCP server/clientTimer[S]- One-shot and repeating timersAsync[S]- Cross-thread wakeupPoll[S]- File descriptor polling
cats-effect integration with typed error channels.
import cats.effect.*
import boilerplate.effect.*
import emile.cats.*
object MyApp extends EmileIOApp:
override def loopConfig: LoopConfig =
LoopConfig.empty.withMetricsEnabled(true)
def run(args: List[String]): IO[ExitCode] =
EmileIOApp.withLoop { loop =>
given Loop = loop
// Resources with typed error channel
TcpResource.bind(address).use { tcp =>
TimerResource.delay(1000).use { _ =>
Eff.succeed(ExitCode.Success)
}
}
}.either.map(_.getOrElse(ExitCode.Error))Resource Types:
TcpResource- Managed TCP handlesTimerResource- Managed timers with delay/intervalAsyncResource- Managed async handlesPollResource- Managed poll handles
Émile uses typed error channels via boilerplate.effect.Eff:
// Eff[F, E, A] ≡ F[Either[E, A]]
type EffIO[A] = Eff[IO, EmileError, A]
// Constructing Eff values
val ok: EffIO[Int] = Eff.succeed[IO, EmileError, Int](42)
val err: EffIO[Int] = Eff.fail[IO, EmileError, Int](EmileError.TimedOut)
// Converting Either to Eff
val tcp: EffIO[Tcp[Open]] = Tcp.init(loop).eff[IO]
// Unwrapping to IO[Either[E, A]]
val io: IO[Either[EmileError, Tcp[Open]]] = tcp.eitherError Types:
AddressError- Address parsing/validation (emile-ipa)EmileError- libuv operations, I/O errors (emile-core/cats)
- Scala: 3.7+
- Scala Native: 0.5+
- libuv: 1.x (system library)
# Compile all modules
sbt compile
# Run tests
sbt test
# Code style check
sbt check
# Auto-format
sbt formatLicensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- libuv - Cross-platform async I/O
- cats-effect - Purely functional effects
- boilerplate - Effect utilities