Cross-Platform Development with Scala: JVM, JavaScript, and Native
Scala's cross-platform capabilities allow developers to write code once and deploy it across multiple platforms: JVM, JavaScript environments, and native platforms. This comprehensive lesson explores how to leverage Scala.js and Scala Native for cross-platform development while maximizing code reuse and maintaining platform-specific optimizations.
Understanding Cross-Platform Architecture
Platform Targets Overview
// Shared code that works across all platforms
// This code can be compiled to JVM bytecode, JavaScript, or native binaries
// Common data structures and business logic
case class User(id: String, name: String, email: String, createdAt: Long)
case class ApiResponse[T](
data: Option[T],
error: Option[String],
timestamp: Long = System.currentTimeMillis()
)
// Shared utilities that work across platforms
object CommonUtils {
def validateEmail(email: String): Boolean = {
email.contains("@") && email.contains(".")
}
def formatTimestamp(timestamp: Long): String = {
// Use Java time API which is available on all platforms
java.time.Instant.ofEpochMilli(timestamp).toString
}
def parseJson[T](json: String)(implicit decoder: JsonDecoder[T]): Either[String, T] = {
try {
Right(decoder.decode(json))
} catch {
case e: Exception => Left(s"JSON parsing failed: ${e.getMessage}")
}
}
def encodeJson[T](value: T)(implicit encoder: JsonEncoder[T]): String = {
encoder.encode(value)
}
}
// Abstract platform interface
trait Platform {
def name: String
def currentTimeMillis(): Long
def randomUUID(): String
def readFile(path: String): Either[String, String]
def writeFile(path: String, content: String): Either[String, Unit]
def httpGet(url: String): Either[String, String]
def httpPost(url: String, body: String): Either[String, String]
}
// Shared business logic
class UserService(platform: Platform) {
private var users = Map.empty[String, User]
def createUser(name: String, email: String): Either[String, User] = {
if (!CommonUtils.validateEmail(email)) {
Left("Invalid email format")
} else {
val userId = platform.randomUUID()
val user = User(userId, name, email, platform.currentTimeMillis())
users = users + (userId -> user)
Right(user)
}
}
def getUser(id: String): Option[User] = users.get(id)
def updateUser(id: String, name: Option[String], email: Option[String]): Either[String, User] = {
users.get(id) match {
case None => Left("User not found")
case Some(user) =>
val updatedUser = user.copy(
name = name.getOrElse(user.name),
email = email.getOrElse(user.email)
)
if (!CommonUtils.validateEmail(updatedUser.email)) {
Left("Invalid email format")
} else {
users = users + (id -> updatedUser)
Right(updatedUser)
}
}
}
def listUsers(): List[User] = users.values.toList.sortBy(_.createdAt)
def saveToFile(path: String): Either[String, Unit] = {
val json = users.values.map(encodeUser).mkString("[", ",", "]")
platform.writeFile(path, json)
}
def loadFromFile(path: String): Either[String, Unit] = {
platform.readFile(path).flatMap { content =>
// Parse JSON and load users
// Implementation would depend on JSON library
Right(())
}
}
private def encodeUser(user: User): String = {
s"""{"id":"${user.id}","name":"${user.name}","email":"${user.email}","createdAt":${user.createdAt}}"""
}
}
// Cross-platform JSON encoding/decoding
trait JsonEncoder[T] {
def encode(value: T): String
}
trait JsonDecoder[T] {
def decode(json: String): T
}
object JsonCodecs {
implicit val userEncoder: JsonEncoder[User] = (user: User) =>
s"""{"id":"${user.id}","name":"${user.name}","email":"${user.email}","createdAt":${user.createdAt}}"""
implicit val userDecoder: JsonDecoder[User] = (json: String) => {
// Simplified JSON parsing - in practice, use a cross-platform JSON library
val trimmed = json.trim.stripPrefix("{").stripSuffix("}")
val fields = trimmed.split(",").map(_.split(":")).map { arr =>
arr(0).trim.stripPrefix("\"").stripSuffix("\"") -> arr(1).trim.stripPrefix("\"").stripSuffix("\"")
}.toMap
User(
id = fields("id"),
name = fields("name"),
email = fields("email"),
createdAt = fields("createdAt").toLong
)
}
implicit def apiResponseEncoder[T](implicit enc: JsonEncoder[T]): JsonEncoder[ApiResponse[T]] = {
(response: ApiResponse[T]) =>
val dataJson = response.data.map(enc.encode).getOrElse("null")
val errorJson = response.error.map(e => s""""$e"""").getOrElse("null")
s"""{"data":$dataJson,"error":$errorJson,"timestamp":${response.timestamp}}"""
}
}
Scala.js: JavaScript Target
Setting Up Scala.js Projects
// build.sbt for Scala.js project
ThisBuild / scalaVersion := "3.3.0"
lazy val commonSettings = Seq(
organization := "com.example",
version := "1.0.0"
)
// Shared code module
lazy val shared = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("shared"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % "2.9.0",
"io.circe" %%% "circe-core" % "0.14.5",
"io.circe" %%% "circe-generic" % "0.14.5",
"io.circe" %%% "circe-parser" % "0.14.5"
)
)
lazy val sharedJvm = shared.jvm
lazy val sharedJs = shared.js
lazy val sharedNative = shared.native
// Scala.js specific module
lazy val client = project
.in(file("client"))
.enablePlugins(ScalaJSPlugin)
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "2.4.0",
"com.raquo" %%% "laminar" % "15.0.1", // Frontend framework
"com.lihaoyi" %%% "upickle" % "3.1.0"
),
scalaJSUseMainModuleInitializer := true,
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }
)
.dependsOn(sharedJs)
// JVM server module
lazy val server = project
.in(file("server"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.5.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0",
"ch.qos.logback" % "logback-classic" % "1.4.7"
)
)
.dependsOn(sharedJvm)
DOM Manipulation and Frontend Development
// Platform-specific implementation for JavaScript
import org.scalajs.dom
import org.scalajs.dom.{document, window, console}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.scalajs.js
import scala.scalajs.js.annotation._
// Scala.js platform implementation
object JSPlatform extends Platform {
val name = "JavaScript"
def currentTimeMillis(): Long = js.Date.now().toLong
def randomUUID(): String = java.util.UUID.randomUUID().toString
def readFile(path: String): Either[String, String] = {
// In browser environment, reading files requires user interaction
Left("File reading not supported in browser environment")
}
def writeFile(path: String, content: String): Either[String, Unit] = {
// Trigger download in browser
val blob = new dom.Blob(js.Array(content), js.Dynamic.literal(`type` = "text/plain"))
val url = dom.URL.createObjectURL(blob)
val link = document.createElement("a").asInstanceOf[dom.html.Anchor]
link.href = url
link.download = path
link.click()
dom.URL.revokeObjectURL(url)
Right(())
}
def httpGet(url: String): Either[String, String] = {
// Synchronous version for simplicity - in practice, use async
try {
val xhr = new dom.XMLHttpRequest()
xhr.open("GET", url, async = false)
xhr.send()
if (xhr.status == 200) {
Right(xhr.responseText)
} else {
Left(s"HTTP error: ${xhr.status}")
}
} catch {
case e: Exception => Left(s"Request failed: ${e.getMessage}")
}
}
def httpPost(url: String, body: String): Either[String, String] = {
try {
val xhr = new dom.XMLHttpRequest()
xhr.open("POST", url, async = false)
xhr.setRequestHeader("Content-Type", "application/json")
xhr.send(body)
if (xhr.status == 200) {
Right(xhr.responseText)
} else {
Left(s"HTTP error: ${xhr.status}")
}
} catch {
case e: Exception => Left(s"Request failed: ${e.getMessage}")
}
}
}
// Async versions using Future
object AsyncJSPlatform {
def httpGetAsync(url: String): Future[Either[String, String]] = {
val promise = scala.concurrent.Promise[Either[String, String]]()
val xhr = new dom.XMLHttpRequest()
xhr.onload = { (_: dom.Event) =>
if (xhr.status == 200) {
promise.success(Right(xhr.responseText))
} else {
promise.success(Left(s"HTTP error: ${xhr.status}"))
}
}
xhr.onerror = { (_: dom.Event) =>
promise.success(Left("Network error"))
}
xhr.open("GET", url)
xhr.send()
promise.future
}
def httpPostAsync(url: String, body: String): Future[Either[String, String]] = {
val promise = scala.concurrent.Promise[Either[String, String]]()
val xhr = new dom.XMLHttpRequest()
xhr.onload = { (_: dom.Event) =>
if (xhr.status == 200) {
promise.success(Right(xhr.responseText))
} else {
promise.success(Left(s"HTTP error: ${xhr.status}"))
}
}
xhr.onerror = { (_: dom.Event) =>
promise.success(Left("Network error"))
}
xhr.open("POST", url)
xhr.setRequestHeader("Content-Type", "application/json")
xhr.send(body)
promise.future
}
}
// Frontend application using Laminar
import com.raquo.laminar.api.L._
import com.raquo.laminar.nodes.ReactiveHtmlElement
import org.scalajs.dom.html
object UserManagementApp {
case class AppState(
users: List[User] = List.empty,
selectedUser: Option[User] = None,
isLoading: Boolean = false,
error: Option[String] = None
)
sealed trait AppAction
case object LoadUsers extends AppAction
case class CreateUser(name: String, email: String) extends AppAction
case class SelectUser(user: User) extends AppAction
case class UpdateUser(id: String, name: String, email: String) extends AppAction
case class DeleteUser(id: String) extends AppAction
case class SetError(error: String) extends AppAction
case object ClearError extends AppAction
class UserStore {
private val userService = new UserService(JSPlatform)
private val stateVar = Var(AppState())
val state: Signal[AppState] = stateVar.signal
def dispatch(action: AppAction): Unit = action match {
case LoadUsers =>
stateVar.update(_.copy(isLoading = true, error = None))
// In a real app, load from server
val users = userService.listUsers()
stateVar.update(_.copy(users = users, isLoading = false))
case CreateUser(name, email) =>
userService.createUser(name, email) match {
case Right(user) =>
stateVar.update(state => state.copy(users = user :: state.users))
case Left(error) =>
stateVar.update(_.copy(error = Some(error)))
}
case SelectUser(user) =>
stateVar.update(_.copy(selectedUser = Some(user)))
case UpdateUser(id, name, email) =>
userService.updateUser(id, Some(name), Some(email)) match {
case Right(updatedUser) =>
stateVar.update(state => state.copy(
users = state.users.map(u => if (u.id == id) updatedUser else u),
selectedUser = Some(updatedUser)
))
case Left(error) =>
stateVar.update(_.copy(error = Some(error)))
}
case DeleteUser(id) =>
stateVar.update(state => state.copy(
users = state.users.filterNot(_.id == id),
selectedUser = state.selectedUser.filter(_.id != id)
))
case SetError(error) =>
stateVar.update(_.copy(error = Some(error)))
case ClearError =>
stateVar.update(_.copy(error = None))
}
}
def createApp(): ReactiveHtmlElement[html.Div] = {
val store = new UserStore()
div(
cls := "user-management-app",
// Header
h1("User Management"),
// Error display
store.state.map(_.error).split(
_ => div(),
error => div(
cls := "error",
s"Error: $error",
button(
"Clear",
onClick --> (_ => store.dispatch(ClearError))
)
)
),
// User form
createUserForm(store),
// User list
div(
cls := "user-list",
h2("Users"),
children <-- store.state.map(_.users).split(_.id)(renderUser(store))
),
// User details
store.state.map(_.selectedUser).split(
_ => div("Select a user to view details"),
user => renderUserDetails(store, user)
)
)
}
private def createUserForm(store: UserStore): ReactiveHtmlElement[html.Form] = {
val nameVar = Var("")
val emailVar = Var("")
form(
cls := "user-form",
h3("Create New User"),
div(
label("Name:"),
input(
tpe := "text",
placeholder := "Enter name",
controlled(
value <-- nameVar,
onInput.mapToValue --> nameVar
)
)
),
div(
label("Email:"),
input(
tpe := "email",
placeholder := "Enter email",
controlled(
value <-- emailVar,
onInput.mapToValue --> emailVar
)
)
),
button(
tpe := "button",
"Create User",
onClick --> { _ =>
store.dispatch(CreateUser(nameVar.now(), emailVar.now()))
nameVar.set("")
emailVar.set("")
}
)
)
}
private def renderUser(store: UserStore)(id: String, initial: User, userSignal: Signal[User]): ReactiveHtmlElement[html.Div] = {
div(
cls := "user-item",
child <-- userSignal.map { user =>
div(
onClick --> (_ => store.dispatch(SelectUser(user))),
div(s"Name: ${user.name}"),
div(s"Email: ${user.email}"),
div(s"Created: ${CommonUtils.formatTimestamp(user.createdAt)}")
)
}
)
}
private def renderUserDetails(store: UserStore, user: User): ReactiveHtmlElement[html.Div] = {
val nameVar = Var(user.name)
val emailVar = Var(user.email)
div(
cls := "user-details",
h3("User Details"),
div(
label("Name:"),
input(
tpe := "text",
controlled(
value <-- nameVar,
onInput.mapToValue --> nameVar
)
)
),
div(
label("Email:"),
input(
tpe := "email",
controlled(
value <-- emailVar,
onInput.mapToValue --> emailVar
)
)
),
div(
button(
"Update",
onClick --> { _ =>
store.dispatch(UpdateUser(user.id, nameVar.now(), emailVar.now()))
}
),
button(
"Delete",
onClick --> { _ =>
store.dispatch(DeleteUser(user.id))
}
)
)
)
}
}
// Main application entry point
@JSExportTopLevel("main")
def main(): Unit = {
val appContainer = document.getElementById("app")
val app = UserManagementApp.createApp()
render(appContainer, app)
}
// JavaScript interop
@js.native
@JSGlobal
object ExternalLibrary extends js.Object {
def someFunction(param: String): String = js.native
}
// Calling JavaScript from Scala.js
object JSInterop {
def callExternalFunction(input: String): String = {
ExternalLibrary.someFunction(input)
}
// Exposing Scala functions to JavaScript
@JSExport
def processData(data: String): String = {
// Process data using Scala logic
data.toUpperCase.reverse
}
// Working with JavaScript promises
def handlePromise(): Future[String] = {
val jsPromise = js.Promise.resolve("Hello from JS")
jsPromise.toFuture
}
}
Advanced Scala.js Features
// Custom JavaScript types
@js.native
trait CustomJSObject extends js.Object {
val name: String = js.native
val value: Double = js.native
def process(): String = js.native
}
// Union types for JavaScript
type StringOrNumber = String | Double
// Working with JavaScript arrays
import scala.scalajs.js.Array as JSArray
object ArrayOperations {
def processJSArray(arr: JSArray[String]): JSArray[String] = {
arr.filter(_.nonEmpty).map(_.toUpperCase)
}
def convertToScala(jsArr: JSArray[String]): List[String] = {
jsArr.toList
}
def convertToJS(scalaList: List[String]): JSArray[String] = {
JSArray(scalaList: _*)
}
}
// Facade for external JavaScript library
@js.native
@JSGlobal("Chart")
class Chart(canvas: dom.html.Canvas, config: ChartConfig) extends js.Object {
def update(): Unit = js.native
def destroy(): Unit = js.native
}
@js.native
trait ChartConfig extends js.Object {
val `type`: String = js.native
val data: ChartData = js.native
val options: js.UndefOr[ChartOptions] = js.native
}
@js.native
trait ChartData extends js.Object {
val labels: JSArray[String] = js.native
val datasets: JSArray[Dataset] = js.native
}
@js.native
trait Dataset extends js.Object {
val label: String = js.native
val data: JSArray[Double] = js.native
val backgroundColor: js.UndefOr[String] = js.native
}
@js.native
trait ChartOptions extends js.Object {
val responsive: js.UndefOr[Boolean] = js.native
val plugins: js.UndefOr[js.Object] = js.native
}
// Using the Chart.js facade
object ChartExample {
def createChart(canvas: dom.html.Canvas): Chart = {
val config = js.Dynamic.literal(
`type` = "bar",
data = js.Dynamic.literal(
labels = JSArray("Jan", "Feb", "Mar", "Apr"),
datasets = JSArray(js.Dynamic.literal(
label = "Sales",
data = JSArray(12, 19, 3, 17),
backgroundColor = "rgba(54, 162, 235, 0.2)"
))
),
options = js.Dynamic.literal(
responsive = true
)
).asInstanceOf[ChartConfig]
new Chart(canvas, config)
}
}
// Web Workers with Scala.js
import org.scalajs.dom.Worker
import scala.concurrent.Promise
object WebWorkerExample {
def createWorker(): Worker = {
val workerCode = """
self.addEventListener('message', function(e) {
const data = e.data;
// Perform heavy computation
const result = heavyComputation(data);
self.postMessage(result);
});
function heavyComputation(data) {
// Simulate heavy work
let sum = 0;
for (let i = 0; i < data.iterations; i++) {
sum += Math.sqrt(i);
}
return { result: sum, processed: data.iterations };
}
"""
val blob = new dom.Blob(js.Array(workerCode), js.Dynamic.literal(`type` = "application/javascript"))
val workerUrl = dom.URL.createObjectURL(blob)
new Worker(workerUrl)
}
def runHeavyComputation(iterations: Int): Future[js.Dynamic] = {
val promise = Promise[js.Dynamic]()
val worker = createWorker()
worker.onmessage = { (e: dom.MessageEvent) =>
promise.success(e.data.asInstanceOf[js.Dynamic])
worker.terminate()
}
worker.onerror = { (e: dom.ErrorEvent) =>
promise.failure(new Exception(e.message))
worker.terminate()
}
val input = js.Dynamic.literal(iterations = iterations)
worker.postMessage(input)
promise.future
}
}
// Service Worker registration
object ServiceWorkerExample {
def registerServiceWorker(): Unit = {
if (js.typeOf(dom.window.navigator.serviceWorker) != "undefined") {
dom.window.navigator.serviceWorker.register("/sw.js").toFuture.foreach { registration =>
console.log("Service Worker registered successfully")
}
}
}
}
Scala Native: Native Platform Target
Setting Up Scala Native Projects
// build.sbt for Scala Native project
ThisBuild / scalaVersion := "3.3.0"
lazy val nativeApp = project
.in(file("native"))
.enablePlugins(ScalaNativePlugin)
.settings(
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "upickle" % "3.1.0",
"com.lihaoyi" %%% "os-lib" % "0.9.1"
),
nativeConfig ~= {
_.withLTO(LTO.thin)
.withMode(Mode.releaseFast)
.withGC(GC.commix)
}
)
.dependsOn(sharedNative)
Native Platform Implementation
// Platform implementation for Scala Native
import scala.scalanative.unsafe._
import scala.scalanative.unsigned._
import scala.scalanative.libc._
import scala.scalanative.posix.unistd
import java.io.{File, FileWriter, FileReader, BufferedReader}
import java.net.{URL, HttpURLConnection}
import scala.io.Source
import scala.util.{Try, Using}
object NativePlatform extends Platform {
val name = "Native"
def currentTimeMillis(): Long = System.currentTimeMillis()
def randomUUID(): String = java.util.UUID.randomUUID().toString
def readFile(path: String): Either[String, String] = {
Try {
Using(Source.fromFile(path))(_.mkString).get
}.toEither.left.map(_.getMessage)
}
def writeFile(path: String, content: String): Either[String, Unit] = {
Try {
Using(new FileWriter(path)) { writer =>
writer.write(content)
}.get
}.toEither.left.map(_.getMessage)
}
def httpGet(url: String): Either[String, String] = {
Try {
val connection = new URL(url).openConnection().asInstanceOf[HttpURLConnection]
connection.setRequestMethod("GET")
connection.setConnectTimeout(5000)
connection.setReadTimeout(5000)
val responseCode = connection.getResponseCode
if (responseCode == 200) {
Using(Source.fromInputStream(connection.getInputStream))(_.mkString).get
} else {
throw new RuntimeException(s"HTTP error: $responseCode")
}
}.toEither.left.map(_.getMessage)
}
def httpPost(url: String, body: String): Either[String, String] = {
Try {
val connection = new URL(url).openConnection().asInstanceOf[HttpURLConnection]
connection.setRequestMethod("POST")
connection.setDoOutput(true)
connection.setRequestProperty("Content-Type", "application/json")
connection.setConnectTimeout(5000)
connection.setReadTimeout(5000)
Using(connection.getOutputStream) { outputStream =>
outputStream.write(body.getBytes("UTF-8"))
}.get
val responseCode = connection.getResponseCode
if (responseCode == 200) {
Using(Source.fromInputStream(connection.getInputStream))(_.mkString).get
} else {
throw new RuntimeException(s"HTTP error: $responseCode")
}
}.toEither.left.map(_.getMessage)
}
}
// Native-specific utilities
object NativeUtils {
// Direct memory management
def allocateMemory(size: Int): Ptr[Byte] = {
stdlib.malloc(size.toULong)
}
def freeMemory(ptr: Ptr[Byte]): Unit = {
stdlib.free(ptr)
}
// C library interop
@extern
object LibC {
def getpid(): CInt = extern
def getppid(): CInt = extern
def gethostname(name: Ptr[CChar], len: CSize): CInt = extern
}
def getCurrentProcessId(): Int = LibC.getpid()
def getParentProcessId(): Int = LibC.getppid()
def getHostname(): String = {
val buffer = stackalloc[CChar](256)
LibC.gethostname(buffer, 256.toULong)
fromCString(buffer)
}
// File system operations
def listDirectory(path: String): Either[String, List[String]] = {
Try {
val dir = new File(path)
if (dir.exists() && dir.isDirectory) {
dir.listFiles().map(_.getName).toList
} else {
throw new RuntimeException(s"Directory does not exist: $path")
}
}.toEither.left.map(_.getMessage)
}
def createDirectory(path: String): Either[String, Unit] = {
Try {
val dir = new File(path)
if (!dir.mkdirs() && !dir.exists()) {
throw new RuntimeException(s"Failed to create directory: $path")
}
}.toEither.left.map(_.getMessage)
}
def deleteFile(path: String): Either[String, Unit] = {
Try {
val file = new File(path)
if (!file.delete()) {
throw new RuntimeException(s"Failed to delete file: $path")
}
}.toEither.left.map(_.getMessage)
}
// Process execution
def executeCommand(command: String, args: String*): Either[String, String] = {
Try {
val processBuilder = new ProcessBuilder((command +: args): _*)
val process = processBuilder.start()
val output = Using(Source.fromInputStream(process.getInputStream))(_.mkString).get
val exitCode = process.waitFor()
if (exitCode == 0) {
output
} else {
val error = Using(Source.fromInputStream(process.getErrorStream))(_.mkString).get
throw new RuntimeException(s"Command failed with exit code $exitCode: $error")
}
}.toEither.left.map(_.getMessage)
}
// Performance monitoring
def getMemoryUsage(): Long = {
Runtime.getRuntime.totalMemory() - Runtime.getRuntime.freeMemory()
}
def getCpuTime(): Long = {
// Implementation would use native system calls
System.nanoTime() // Simplified
}
}
// Command-line application
object NativeApp {
case class Config(
action: String = "",
inputFile: Option[String] = None,
outputFile: Option[String] = None,
verbose: Boolean = false
)
def parseArgs(args: Array[String]): Either[String, Config] = {
args.toList match {
case Nil => Left("No arguments provided")
case action :: rest =>
val config = rest.foldLeft(Config(action = action)) { (cfg, arg) =>
arg match {
case s if s.startsWith("--input=") => cfg.copy(inputFile = Some(s.drop(8)))
case s if s.startsWith("--output=") => cfg.copy(outputFile = Some(s.drop(9)))
case "--verbose" => cfg.copy(verbose = true)
case _ => cfg
}
}
Right(config)
}
}
def processUsers(config: Config): Either[String, Unit] = {
val userService = new UserService(NativePlatform)
config.action match {
case "create" =>
val user = userService.createUser("John Doe", "john@example.com")
user.map { u =>
if (config.verbose) {
println(s"Created user: ${u.name} (${u.id})")
}
}
case "list" =>
val users = userService.listUsers()
users.foreach { user =>
println(s"${user.id}: ${user.name} <${user.email}>")
}
Right(())
case "export" =>
config.outputFile match {
case Some(file) =>
userService.saveToFile(file).map { _ =>
if (config.verbose) {
println(s"Users exported to $file")
}
}
case None => Left("Output file required for export")
}
case "import" =>
config.inputFile match {
case Some(file) =>
userService.loadFromFile(file).map { _ =>
if (config.verbose) {
println(s"Users imported from $file")
}
}
case None => Left("Input file required for import")
}
case unknown => Left(s"Unknown action: $unknown")
}
}
def main(args: Array[String]): Unit = {
parseArgs(args) match {
case Left(error) =>
System.err.println(s"Error: $error")
println("Usage: app <action> [options]")
println("Actions: create, list, export, import")
println("Options: --input=file, --output=file, --verbose")
System.exit(1)
case Right(config) =>
processUsers(config) match {
case Left(error) =>
System.err.println(s"Error: $error")
System.exit(1)
case Right(_) =>
if (config.verbose) {
println("Operation completed successfully")
}
}
}
}
}
// Native library integration
@link("sqlite3")
@extern
object SQLite {
type Sqlite3 = Ptr[Byte]
type Sqlite3Stmt = Ptr[Byte]
def sqlite3_open(filename: CString, ppDb: Ptr[Sqlite3]): CInt = extern
def sqlite3_close(db: Sqlite3): CInt = extern
def sqlite3_prepare_v2(db: Sqlite3, zSql: CString, nByte: CInt, ppStmt: Ptr[Sqlite3Stmt], pzTail: Ptr[CString]): CInt = extern
def sqlite3_step(stmt: Sqlite3Stmt): CInt = extern
def sqlite3_finalize(stmt: Sqlite3Stmt): CInt = extern
def sqlite3_column_text(stmt: Sqlite3Stmt, iCol: CInt): CString = extern
def sqlite3_column_int(stmt: Sqlite3Stmt, iCol: CInt): CInt = extern
}
// SQLite database implementation
class NativeDatabase {
import SQLite._
private var db: Sqlite3 = _
def open(filename: String): Either[String, Unit] = {
Zone { implicit z =>
val dbPtr = stackalloc[Sqlite3]()
val result = sqlite3_open(toCString(filename), dbPtr)
if (result == 0) {
db = !dbPtr
Right(())
} else {
Left(s"Failed to open database: $result")
}
}
}
def close(): Unit = {
if (db != null) {
sqlite3_close(db)
db = null
}
}
def execute(sql: String): Either[String, Unit] = {
Zone { implicit z =>
val stmtPtr = stackalloc[Sqlite3Stmt]()
val result = sqlite3_prepare_v2(db, toCString(sql), -1, stmtPtr, null)
if (result == 0) {
val stmt = !stmtPtr
val stepResult = sqlite3_step(stmt)
sqlite3_finalize(stmt)
if (stepResult == 101) { // SQLITE_DONE
Right(())
} else {
Left(s"SQL execution failed: $stepResult")
}
} else {
Left(s"SQL preparation failed: $result")
}
}
}
def query(sql: String): Either[String, List[Map[String, String]]] = {
// Implementation for querying data
Right(List.empty) // Simplified
}
}
Code Sharing Strategies
Cross-Platform Architecture Patterns
// Abstract factory pattern for platform-specific implementations
trait PlatformFactory {
def createFileSystem(): FileSystemOps
def createNetworking(): NetworkOps
def createCrypto(): CryptoOps
def createLogger(): Logger
}
object JVMFactory extends PlatformFactory {
def createFileSystem(): FileSystemOps = new JVMFileSystem()
def createNetworking(): NetworkOps = new JVMNetworking()
def createCrypto(): CryptoOps = new JVMCrypto()
def createLogger(): Logger = new JVMLogger()
}
object JSFactory extends PlatformFactory {
def createFileSystem(): FileSystemOps = new JSFileSystem()
def createNetworking(): NetworkOps = new JSNetworking()
def createCrypto(): CryptoOps = new JSCrypto()
def createLogger(): Logger = new JSLogger()
}
object NativeFactory extends PlatformFactory {
def createFileSystem(): FileSystemOps = new NativeFileSystem()
def createNetworking(): NetworkOps = new NativeNetworking()
def createCrypto(): CryptoOps = new NativeCrypto()
def createLogger(): Logger = new NativeLogger()
}
// Platform abstraction interfaces
trait FileSystemOps {
def readText(path: String): Either[String, String]
def writeText(path: String, content: String): Either[String, Unit]
def exists(path: String): Boolean
def listFiles(directory: String): Either[String, List[String]]
def createDirectory(path: String): Either[String, Unit]
def delete(path: String): Either[String, Unit]
}
trait NetworkOps {
def httpGet(url: String, headers: Map[String, String] = Map.empty): Either[String, HttpResponse]
def httpPost(url: String, body: String, headers: Map[String, String] = Map.empty): Either[String, HttpResponse]
def httpPut(url: String, body: String, headers: Map[String, String] = Map.empty): Either[String, HttpResponse]
def httpDelete(url: String, headers: Map[String, String] = Map.empty): Either[String, HttpResponse]
}
trait CryptoOps {
def hash(data: String, algorithm: String = "SHA-256"): String
def encrypt(data: String, key: String): Either[String, String]
def decrypt(encryptedData: String, key: String): Either[String, String]
def generateKey(): String
}
trait Logger {
def debug(message: String): Unit
def info(message: String): Unit
def warn(message: String): Unit
def error(message: String): Unit
def error(message: String, throwable: Throwable): Unit
}
case class HttpResponse(
status: Int,
body: String,
headers: Map[String, String]
)
// Cross-platform application framework
abstract class CrossPlatformApp {
protected def platformFactory: PlatformFactory
lazy val fileSystem: FileSystemOps = platformFactory.createFileSystem()
lazy val networking: NetworkOps = platformFactory.createNetworking()
lazy val crypto: CryptoOps = platformFactory.createCrypto()
lazy val logger: Logger = platformFactory.createLogger()
def initialize(): Either[String, Unit]
def run(args: List[String]): Either[String, Unit]
def shutdown(): Unit
final def main(args: Array[String]): Unit = {
logger.info("Application starting...")
val result = for {
_ <- initialize()
_ <- run(args.toList)
} yield ()
result match {
case Left(error) =>
logger.error(s"Application failed: $error")
System.exit(1)
case Right(_) =>
logger.info("Application completed successfully")
}
shutdown()
}
}
// Shared business logic implementation
class UserManagementApp(val platformFactory: PlatformFactory) extends CrossPlatformApp {
private val userService = new UserService(new PlatformAdapter(platformFactory))
def initialize(): Either[String, Unit] = {
logger.info("Initializing user management application")
// Create necessary directories
fileSystem.createDirectory("data").flatMap { _ =>
fileSystem.createDirectory("logs")
}
}
def run(args: List[String]): Either[String, Unit] = {
args match {
case "server" :: port :: Nil =>
startServer(port.toInt)
case "cli" :: command :: rest =>
runCliCommand(command, rest)
case "import" :: file :: Nil =>
importUsers(file)
case "export" :: file :: Nil =>
exportUsers(file)
case _ =>
Left("Usage: app <server|cli|import|export> [args...]")
}
}
def shutdown(): Unit = {
logger.info("Shutting down application")
}
private def startServer(port: Int): Either[String, Unit] = {
logger.info(s"Starting server on port $port")
// Platform-specific server implementation would go here
Right(())
}
private def runCliCommand(command: String, args: List[String]): Either[String, Unit] = {
command match {
case "create" if args.length >= 2 =>
val name = args(0)
val email = args(1)
userService.createUser(name, email).map { user =>
logger.info(s"Created user: ${user.name}")
}
case "list" =>
val users = userService.listUsers()
users.foreach { user =>
println(s"${user.id}: ${user.name} <${user.email}>")
}
Right(())
case "update" if args.length >= 3 =>
val id = args(0)
val name = args(1)
val email = args(2)
userService.updateUser(id, Some(name), Some(email)).map { user =>
logger.info(s"Updated user: ${user.name}")
}
case _ =>
Left("Invalid CLI command")
}
}
private def importUsers(file: String): Either[String, Unit] = {
userService.loadFromFile(file)
}
private def exportUsers(file: String): Either[String, Unit] = {
userService.saveToFile(file)
}
}
// Platform adapter
class PlatformAdapter(factory: PlatformFactory) extends Platform {
private val fs = factory.createFileSystem()
private val net = factory.createNetworking()
private val crypto = factory.createCrypto()
val name: String = "Cross-Platform"
def currentTimeMillis(): Long = System.currentTimeMillis()
def randomUUID(): String = java.util.UUID.randomUUID().toString
def readFile(path: String): Either[String, String] = fs.readText(path)
def writeFile(path: String, content: String): Either[String, Unit] = fs.writeText(path, content)
def httpGet(url: String): Either[String, String] =
net.httpGet(url).map(_.body)
def httpPost(url: String, body: String): Either[String, String] =
net.httpPost(url, body).map(_.body)
}
Build Configuration and Automation
// Multi-platform build configuration
// build.sbt
ThisBuild / scalaVersion := "3.3.0"
ThisBuild / organization := "com.example"
ThisBuild / version := "1.0.0"
lazy val commonSettings = Seq(
scalacOptions ++= Seq(
"-deprecation",
"-feature",
"-unchecked",
"-Xlint",
"-Ywarn-dead-code",
"-Ywarn-numeric-widen"
)
)
// Shared modules
lazy val shared = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/shared"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % "2.9.0",
"org.typelevel" %%% "cats-effect" % "3.5.0",
"io.circe" %%% "circe-core" % "0.14.5",
"io.circe" %%% "circe-generic" % "0.14.5",
"io.circe" %%% "circe-parser" % "0.14.5"
)
)
.jvmSettings(
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.2.15" % Test
)
)
.jsSettings(
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "2.4.0"
)
)
.nativeSettings(
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "os-lib" % "0.9.1"
)
)
lazy val sharedJvm = shared.jvm
lazy val sharedJs = shared.js
lazy val sharedNative = shared.native
// JVM server application
lazy val server = project
.in(file("modules/server"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http" % "10.5.0",
"com.typesafe.akka" %% "akka-stream" % "2.8.0",
"com.typesafe.akka" %% "akka-actor-typed" % "2.8.0",
"ch.qos.logback" % "logback-classic" % "1.4.7",
"com.h2database" % "h2" % "2.1.214"
),
Docker / packageName := "user-management-server",
dockerBaseImage := "openjdk:17-jre-slim"
)
.enablePlugins(JavaAppPackaging, DockerPlugin)
.dependsOn(sharedJvm)
// JavaScript client application
lazy val client = project
.in(file("modules/client"))
.enablePlugins(ScalaJSPlugin)
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "2.4.0",
"com.raquo" %%% "laminar" % "15.0.1"
),
scalaJSUseMainModuleInitializer := true,
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.ESModule)
.withOptimizer(true)
},
Assets / pipelineStages := Seq(scalaJSPipeline)
)
.dependsOn(sharedJs)
// Native CLI application
lazy val cli = project
.in(file("modules/cli"))
.enablePlugins(ScalaNativePlugin)
.settings(commonSettings)
.settings(
nativeConfig ~= {
_.withLTO(LTO.thin)
.withMode(Mode.releaseFast)
.withGC(GC.commix)
},
nativeLinkStubs := true
)
.dependsOn(sharedNative)
// Testing across platforms
lazy val tests = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("modules/tests"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % "3.2.15" % Test,
"org.scalatestplus" %%% "scalacheck-1-17" % "3.2.15.0" % Test
)
)
.dependsOn(shared)
lazy val testsJvm = tests.jvm
lazy val testsJs = tests.js
lazy val testsNative = tests.native
// Custom SBT tasks for multi-platform operations
lazy val buildAll = taskKey[Unit]("Build all platform targets")
lazy val testAll = taskKey[Unit]("Test all platform targets")
lazy val packageAll = taskKey[Unit]("Package all platform targets")
buildAll := {
(sharedJvm / compile).value
(sharedJs / compile).value
(sharedNative / compile).value
(server / compile).value
(client / compile).value
(cli / compile).value
}
testAll := {
(testsJvm / test).value
(testsJs / test).value
(testsNative / test).value
}
packageAll := {
(server / Docker / publishLocal).value
(client / fastOptJS).value
(cli / nativeLink).value
}
// CI/CD configuration
// .github/workflows/ci.yml
/*
name: CI
on: [push, pull_request]
Comments
Be the first to comment on this lesson!