スパイダープラス Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

Universal Links によるAppコンテンツへのアクセスが可能になりました

はじめに

こんにちは、スパイダープラス開発メンバーのちょっくんです。 みなさんはディープリンクという手法をご存知でしょうか? ウェブリンクからアプリを起動して特定の画面を表示させる、アレです。 ディープリンクにはいくつか手法があり、SPIDER+では長らくカスタムURLスキームを採用していました。しかし、この手法ではGMailでリンクを受け取った場合に、アプリを起動できなくなっていることがわかりました。 そこでiOS9以上で推奨されるUniversal Linksを採用したことについて紹介します。

続きを読む

AIレビューで負担を半減した方法:GitHub Copilotの活用事例

1. レビュー体制の限界とAIレビューへの期待

スパイダープラスでWeb開発を担当している高森です。

今回は、直近の開発フローの課題であった「コードレビュー工数の増大」を解決するために、AIレビューを本格導入し、開発体制をどう変革したかについてお話しします。

これまでの開発体制と課題

スパイダープラスのWeb開発チームでは、コードの品質を担保するため、Pull Request (以下PR) のレビューを必ず2名体制で行っていました。

続きを読む

クロスプラットフォームアプリ開発の新星「Swift SDK for Android」試してみた

はじめに

こんにちは!ピヨコです。

今回は2025年10月末に発表された「Swift SDK for Android」を使ってSwift製のライブラリをAndroidアプリで試していきたいと思います。
www.swift.org

今回はSwift.orgが提供しているサンプルを利用します。
github.com

旧来のスマホアプリ開発において、SwiftではiOSアプリしか開発できませんでした。
そのため、iOSAndroidクロスプラットフォームを実現するためには、iOS/AndroidのそれぞれのNativeアプリを個別で開発する、もしくはFlutterやReact Nativeといったフレームワークを採用する必要がありました。
今回のSDKの発表によって、既存のSwiftで動作しているiOSアプリがあればAndroidアプリとしてもSwiftの資産を利用できる可能性が高まります。
現在はプレビュー版ですので、今後のアップデートや情報の追加に期待したいところです。

※記事公開時点ではプレビュー版のSDKです。今後のSDKのアップデートによって手順など変更の可能性があります。必ず最新の情報を確認してください。

「Swift SDK for Android」とは?

「Swift SDK for Android」はOSSのコミュニティであるSwift.orgのAndroid workgroupによって開発されたSDKです。

「Swift SDK for Android」の仕組みとしては、「Swift Java」を利用しています。
「Swift Java」は、SwiftのプログラムからJavaのコードやライブラリを呼び出したり、逆にJavaからSwiftのコードを呼び出したりできるようにするためのライブラリとツールセットです。
github.com

大きな特徴としては、WindowsLinuxからも「Swift SDK for Android」を利用して開発可能ということです。
ただし、iOSアプリとして動作確認やストア公開するためには引き続きmacOSXcodeが必要になります。

また、現時点ではSwiftとJavaの相互変換のみで、UIKitやSwiftUIなどのFrameworkには対応できていません。
つまり、Swiftで書かれたロジック部分はJavaに変換しての活用が可能ですが、Viewについては従来通りそれぞれのプラットフォームでコードを持つ必要があります。

記事の内容

  • Swift SDK for Android利用のための環境構築
  • サンプルのアプリを動かしてみる
  • 自作のSwift製のライブラリを作ってAndroidアプリで動かしてみる

説明しないこと

実行環境

  • macOS Tahoe 26.0.1
    • 空き容量が5GB以上必要
    • ターミナルはzshを利用
  • Swift 6.3-dev (main-snapshot-2025-10-17)
  • Swift SDK for Android (DEVELOPMENT-SNAPSHOT-2025-10-17-a)
  • Java Development Kit (JDK) 25.0.1
  • Xcode 26.1.1
  • Android Studio 2025.2.1 Patch 1
    • 2025年11月時点で最新版ではないと正常に動作しないようです

導入

Getting Started with the Swift SDK for Androidに従って進めていきます。
2025-10-17のスナップショットが最新のようだったので、バージョンの「2025-10-16」を「2025-10-17」に置き換えて導入を行なっています。

手順はこの4ステップで進めていきます。

  1. Swiftlyの導入
  2. Host Toolchainの導入
  3. Swift SDK for Androidの導入
  4. Android NDKの導入
  5. Java Development Kit (JDK)の導入

Swiftlyの導入

Swiftly経由で特定バージョンのSwiftを利用する必要があるため、Swiftlyを導入します。
Xcodeが入っている場合はすでにSwiftがインストールされていますが、バージョンコントロールに必要なので必ず導入してください。

Install SwiftでOSごとの導入方法が書かれているので参考にしてください。

今回はMacなのでターミナルからこちらのコマンドを実行し、完了したらターミナルを再起動します。

% curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && \
  installer -pkg swiftly.pkg -target CurrentUserHomeDirectory && \
  ~/.swiftly/bin/swiftly init --quiet-shell-followup && \
  . "${SWIFTLY_HOME_DIR:-$HOME/.swiftly}/env.sh" && \
  hash -r

Host Toolchainの導入

SDKに対応したバージョンのSwiftを導入するため、以下を順番に実行します。

% swiftly install main-snapshot-2025-10-17
% swiftly use main-snapshot-2025-10-17
% swiftly run swift --version

最後にこのような表示が出ればOKです。

Apple Swift version 6.3-dev (LLVM d8e7cc748ee6e7f, Swift a07ea37d0054945)

Swift SDK for Androidの導入

次にSDKを導入するため、以下を順番に実行します。

% swift sdk install https://download.swift.org/development/android-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-17-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-17-a_android-0.1.artifactbundle.tar.gz --checksum ba57d4663be0ebba2a22d8377a282a37d2d801aa25e5c5ed4b8d9b7769ae6782
% swiftly run swift sdk list

最後にこのような表示が出ればOKです。

swift-DEVELOPMENT-SNAPSHOT-2025-10-17-a-android-0.1

Android NDKの導入

次にAndroid NDKを導入します。Android NDKは、AndroidでCやC++のコードを利用するためのツールセットです。
以下を順番に実行します。

% mkdir ~/android-ndk
% cd ~/android-ndk
% curl -fSLO https://dl.google.com/android/repository/android-ndk-r27d-$(uname -s).zip
% unzip -q android-ndk-r27d-*.zip
% export ANDROID_NDK_HOME=$PWD/android-ndk-r27d
% cd ~/Library/org.swift.swiftpm || cd ~/.swiftpm
% ./swift-sdks/swift-DEVELOPMENT-SNAPSHOT-2025-10-17-a-android-0.1.artifactbundle/swift-android/scripts/setup-android-sdk.sh

最後にこのような表示が出ればOKです。

setup-android-sdk.sh: success: ndk-sysroot linked to Android NDK at /Users/hoge/android-ndk/android-ndk-r27d/toolchains/llvm/prebuilt

Java Development Kit (JDK)の導入

最後にJDKを導入します。すでにJDKを導入済みの場合はバージョンや環境変数JAVA_HOMEの設定を確認してください。
将来的には不要になるとのことですが、現時点ではJDK 25が推奨とのことです。
以下を順番に実行します。今回はbrewで導入しますが、miseやsdkmanなど普段お使いのものがあればそちらで構いません。

% brew install openjdk
% sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk
% echo 'export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"' >> ~/.zshrc
% source ~/.zshrc
% java --version

このような表示が出ればJDKの導入はOKです。ここで表示されたバージョンを控えておき、続けてJAVA_HOMEを設定します。

openjdk 25.0.1 2025-10-21
OpenJDK Runtime Environment Homebrew (build 25.0.1)
OpenJDK 64-Bit Server VM Homebrew (build 25.0.1, mixed mode, sharing)

JAVA_HOMEのバージョンの25.0.1のところは適宜上記で控えたバージョンに差し替えて実行してください。

% echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 25.0.1)' >> ~/.zshrc
% source ~/.zshrc

サンプルを動かす

さて、実際に公開されているサンプルを動かしてみたいと思います。
https://github.com/swiftlang/swift-android-examples/

hello-swift-java/の配下には、Androidアプリとアプリに導入するSwiftのライブラリがそれぞれ配置されています。

swift-android-example
├── hello-swift-java
│   ├── hashing-app   // Androidアプリの実装
│   ├── hashing-lib   // Swiftのライブラリ
│   ├── README.md
│   └── resources     // READMEの画像

Swift製ライブラリをAndroidプロジェクトから利用可能にする

サンプルのソースをローカルにクローンもしくはダウンロードしたら、README.mdの「Publish swift-java packages locally」の通りに進めていきます。
JDK 25の導入とJAVA_HOMEの環境変数の設定は終わっているのでその次から進めます。

まずはhashing-libの依存関係のあるPackageを取得しておきます。

% cd hashing-lib
% swift package resolve

取得したswift-javaAndroidプロジェクトから利用できるようにローカルのMavenRepositoryに公開します。

% ./.build/checkouts/swift-java/gradlew --project-dir .build/checkouts/swift-java :SwiftKitCore:publishToMavenLocal

バージョンを導入時のものに合わせる

完了したら、swift-android-exampleAndroid Studioで開きます。

このままだとSyncが失敗するため、バージョン関連を適宜変更します。

まず、Settings > Build, Execution, Deployment > Build Tools > Gradle からGradle JDKJAVA_HOMEに変えておきます。

次にhello-swift-java/hashing-lib/build.gradleswift-android.gradle.ktsのSwiftバージョンとSDKのバージョンをそれぞれ今回導入したものに変えておきます。
バージョン番号等は導入したものに適宜読み替えてください。

diff --git a/hello-swift-java/hashing-lib/build.gradle b/hello-swift-java/hashing-lib/build.gradle
index f0170c9..b0fc4f6 100644
--- a/hello-swift-java/hashing-lib/build.gradle
+++ b/hello-swift-java/hashing-lib/build.gradle
@@ -99,8 +99,8 @@ def swiftRuntimeLibs = [
     "swiftSynchronization"
 ]
 
-def sdkName = "swift-DEVELOPMENT-SNAPSHOT-2025-10-16-a-android-0.1.artifactbundle"
-def swiftVersion = "main-snapshot-2025-10-16"
+def sdkName = "swift-DEVELOPMENT-SNAPSHOT-2025-10-17-a-android-0.1.artifactbundle"
+def swiftVersion = "main-snapshot-2025-10-17"
 def minSdk = android.defaultConfig.minSdkVersion.apiLevel
 /**
  * Android ABIs and their Swift triple mappings
diff --git a/swift-android.gradle.kts b/swift-android.gradle.kts
index aef7792..de12330 100644
--- a/swift-android.gradle.kts
+++ b/swift-android.gradle.kts
@@ -14,8 +14,8 @@ data class SwiftConfig(
     var releaseExtraBuildFlags: List<String> = emptyList(),
     var swiftlyPath: String? = null, // Optional custom swiftly path
     var swiftSDKPath: String? = null, // Optional custom Swift SDK path
-    var swiftVersion: String = "main-snapshot-2025-10-16", // Swift version
-    var androidSdkVersion: String = "DEVELOPMENT-SNAPSHOT-2025-10-16-a-android-0.1" // SDK version
+    var swiftVersion: String = "main-snapshot-2025-10-17", // Swift version
+    var androidSdkVersion: String = "DEVELOPMENT-SNAPSHOT-2025-10-17-a-android-0.1" // SDK version
 )

Androidアプリを動かす

Syncが完了したら、実際にBuild、Runしていきます。
Configurationは「hello-swift-java-hashing-app」を指定します。  

無事にアプリを動かすことができました🎉

Swift製のライブラリを作ってAndroidアプリで動かしてみる

hashing-libを参考に新しくライブラリを作ってみます。
inputに対して「こんにちは!{input}さん!」のような文字列を返す簡単な内容です。

Swiftライブラリの作成

まずはSwiftPackageを作成します。

hello-swift-java
├── hello-lib
│   ├── build.gradle
│   ├── gradle.properties
│   ├── Package.swift
│   ├── Sources
│   │   └── SwiftHello
│   │       ├── swift-java.config
│   │       └── SwiftHello.swift
│   └── Tests
│       └── SwiftHelloTests

Package.swiftはhash-libのものからPackageの設定を書き換えます。
全て掲載すると長いので変更部分だけ抜粋します。

Package.swift

...
...

let package = Package(
  name: "SwiftHello",
  platforms: [.macOS(.v15), .iOS(.v16)],
  products: [
    .library(
      name: "SwiftHello",
      type: .dynamic,
      targets: ["SwiftHello"])
  ],
  dependencies: [
    .package(url: "https://github.com/swiftlang/swift-java", branch: "main"),
    .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
  ],
  targets: [
    .target(
      name: "SwiftHello",
      dependencies: [
        .product(name: "Crypto", package: "swift-crypto"),
        .product(name: "SwiftJava", package: "swift-java"),
        .product(name: "CSwiftJavaJNI", package: "swift-java"),
        .product(name: "SwiftJavaRuntimeSupport", package: "swift-java"),
      ],
      swiftSettings: [
        .swiftLanguageMode(.v5),
        .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows]))
      ],
      plugins: [
        .plugin(name: "JExtractSwiftPlugin", package: "swift-java")
      ]
    ),
    .testTarget(
      name: "SwiftHelloTests",
      dependencies: ["SwiftHello"]
    ),
  ]
)

build.gradleはhash-libのものからnamespaceとソースのディレクトリのパスを書き換えます。
全て掲載すると長いのでこちらも変更部分だけ抜粋します。

build.gradle

...
...

android {
    namespace "com.example.hellolib"
    compileSdkVersion 34

    defaultConfig {
        minSdkVersion 28
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

...
...

def buildSwiftAll = tasks.register("buildSwiftAll") {
    group = "build"
    description = "Builds the Swift code for all Android ABIs."

    // If the package description changes, we should execute jextract again, maybe we added jextract to new targets
    inputs.file(new File(projectDir, "Package.swift"))
    inputs.dir(new File(layout.projectDirectory.asFile, "Sources/SwiftHello".toString()))

    outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}"))

    File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile
    if (!baseSwiftPluginOutputsDir.exists()) {
        baseSwiftPluginOutputsDir.mkdirs()
    }
    Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each {
        // Add any Java sources generated by the plugin to our sourceSet
        if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) {
            outputs.dir(it)
        }
    }
}

ライブラリの実装です。
実装後はswift build -vでビルドが通るか確認しておきます。

Sources/SwiftHello/SwiftHello.swift  

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

public func hello(_ input: String) -> String {
    return "こんにちは!\(input)さん!"
}

Javaから参照するパッケージ名の設定です。
hash-libのものからjavaPackageだけ書き換えます。

swift-java.config

{
  "javaPackage": "com.example.swifthello",
  "mode": "jni"
}

Swiftライブラリのビルド

一通り準備ができたら、ライブラリをAndroidアプリで使えるようビルドします。
サンプルソースのルートのsettings.gradle.ktsAndroidアプリで参照するPackage名とパスを追加します。

diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0be2778..e758263 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,6 +28,8 @@ include(":hello-swift-java-hashing-lib")
 project(":hello-swift-java-hashing-lib").projectDir = file("hello-swift-java/hashing-lib")
 include(":hello-swift-java-hashing-app")
 project(":hello-swift-java-hashing-app").projectDir = file("hello-swift-java/hashing-app")
+include(":hello-swift-java-hello-lib")
+project(":hello-swift-java-hello-lib").projectDir = file("hello-swift-java/hello-lib")

サンプルソースのルート、にgradlewがあるのでそちらからビルドを実行します。

% cd swift-android-example
% ./gradlew :hello-swift-java-hello-lib:assembleRelease

Androidアプリからの実行

サンプルのアプリのbuild.gradle.ktsにライブラリの依存関係を追加してSyncします。

diff --git a/hello-swift-java/hashing-app/build.gradle.kts b/hello-swift-java/hashing-app/build.gradle.kts
index e087f09..87f86fc 100644
--- a/hello-swift-java/hashing-app/build.gradle.kts
+++ b/hello-swift-java/hashing-app/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
     implementation(libs.androidx.material3)
     implementation("org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT")
     implementation(project(":hello-swift-java-hashing-lib"))
+    implementation(project(":hello-swift-java-hello-lib"))
     testImplementation(libs.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)

Syncが完了したら、Androidアプリ側のソースを書き換えます。
挙動を試したいだけなので、既存のhash値を表示する部分をそのまま書き換えてます。

...
...
import com.example.swifthello.SwiftHello

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            HashingAppTheme {
                Surface (
                    modifier = Modifier.fillMaxSize().padding(top = 64.dp),
                    color = MaterialTheme.colorScheme.background
                ) {
                    HelloScreen()
                }
            }
        }
    }
}

@Composable
fun HelloScreen() {
    val input = remember { mutableStateOf("") }
    val helloResult = remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        TextField(
            value = input.value,
            onValueChange = { input.value = it },
            label = { Text("Enter name") },
            modifier = Modifier.fillMaxWidth()
        )

        Button(
            colors = ButtonDefaults.buttonColors(
                containerColor = Color(0xFFF05138),
                contentColor = Color.White
            ),
            onClick = {
                helloResult.value = SwiftHello.hello(input.value)
            }
        ) {
            Text("Hello")
        }

        if (helloResult.value.isNotEmpty()) {
            Text(
                text = helloResult.value,
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

BuildしてRunします。
新しいSwiftライブラリでも無事にアプリを動かすことができました🎉

まとめ

今回は10月末に発表された「Swift SDK for Android」を実際に触ってみました。

現在はプレビュー版なのでやや環境構築の手間が多い印象でしたが、今後の開発で改善されるとのことです。
また、検証中にSwift→Javaの変換が上手くいかないことがありましたが、UIKitやSwiftUIも未対応のことから今後の改善に期待です。
(余談:DateFormatterが変換できず、Swiftライブラリを書き直してます。)

今回簡単な検証をしただけですが、今後の開発の進展によってはクロスプラットフォームアプリの開発のフレームワーク選択に大きな影響を及ぼすだろうことが予想できます。今後のアップデートや情報の追加が楽しみです。

スパイダープラスでは、エンジニアを募集しています。興味のある方はぜひ、カジュアル面談からでもお気軽にご応募ください!

TypeScriptとの比較で解き明かすRustのメモリ管理

皆様こんにちは。現在スパイダープラスはフロントエンド・バックエンドにTypescriptを用いて、型安全なWebアプリケーション開発を行っています。

TypeScriptを用いることで、型の安全性を確保しつつ、高い生産性で開発を進められることが実感できています。

一方で、安全性やパフォーマンスを重視するシステム開発にRustを採用する試みを進めています。

Rustは、TypeScriptとは異なるアプローチでメモリ管理と安全性を実現しています。

この記事では、TypeScript/JavaScriptを基準にしながら、Rustが保証する「メモリ安全性」とは何なのかを探ります。メモリ管理の仕組みなど「安全性」に対する考え方の違いを解説します。


TypeScriptのメモリ管理の仕組み

まず、TypeScript/JavaScriptでコーディングする時どのような仕組みとなっているかを解説します。

TypeScript/JavaScriptでコーディングするとき、ガベージコレクタ(GC)が定期的にゴミをメモリから掃除してくれるので、「このオブジェクトが使われなくなったら、メモリを解放しよう…」と考えずに開発を進められます。

では、GCは具体的にどうやって「ゴミ」を見つけ出すのでしょうか?

これはマーク&スイープ(Mark and Sweep)というアルゴリズムによって実現されています。

マーク&スイープとは簡単に説明すると、まずプログラムのルート(グローバル変数や関数の引数など)から辿れるオブジェクトを「マーク」し、次にマークされなかったオブジェクトを「スイープ(解放)」するという仕組みです。以下、ステップで解説します。

ステップ1: マークフェーズ

  • 下図で説明します。プログラムの「ルート」から開始し、参照をたどります。下図では globalがルートに相当し、グローバル変数、現在実行中の関数のローカル変数、関数の引数など、確実にアクセス可能な場所を表します。
  • ルートから辿れるすべてのオブジェクト(図中の node1node9)を再帰的に探索し、「到達可能なもの」にマークをつけます。これらのノード間には相互参照や循環参照があっても、ルートから辿れる限りすべてマークされます。

ステップ2: スイープフェーズ

  • 次に、メモリ上のすべてのオブジェクトをスキャンし、マークがついていないオブジェクト(=どこからも参照されていないゴミ)を見つけます。図中では「削除対象 (unreachables)」のサブグラフ内にある unr1unr2unr3がこれに該当します。これらのノードは互いに参照し合っていても、globalから到達できないため削除対象となります。
  • それらのメモリ領域を解放し、再利用可能にするという仕組みとなります。

マーク・アンド・スイープ

この仕組みによって、開発者が明示的に deletefreeのような命令を書かなくても、使われなくなったメモリは自動的に掃除されます。

これにより、開発者はメモリ管理を意識することなく開発を進めることができ、アプリケーションのビジネスロジックに集中することができます。これはアプリケーション開発においては大きなメリットとなり、高い生産性をもたらすことが期待されます。


Rust - メモリ領域「スタック」「ヒープ」「静的領域」

TypeScriptと違い、RustはGCを持ちません。その代わり、プログラムが利用する主要なメモリ領域、「スタック」、「ヒープ」、「静的領域」を意識して開発を行う必要があります。

なぜメモリ領域を使い分ける必要があるのか?

なぜわざわざメモリ領域を「スタック」「ヒープ」「静的領域」と分けて使う必要があるのでしょうか?

それは、Rustがコンパイル時にメモリ配置を確定させる必要があるからです。

TypeScript/JavaScriptではGCが実行時に動的にメモリを管理してくれますが、Rustには実行時のGCがありません。その代わり、コンパイル時に「どのデータをどこに配置し、いつ解放するか」を決定します。これにより、実行時のオーバーヘッドなしで安全なメモリ管理を実現しているのです。

そのためRustでは、すべての型のサイズがコンパイル時に確定している必要がありますコンパイラが「この変数にはN バイト必要だ」と事前に知ることができれば、スタックに効率的に配置できます。逆に、実行時にしかサイズが分からないデータ(可変長の文字列や配列など)は、柔軟に伸縮できるヒープ領域に配置する必要があるのです。

一見面倒に思えるこの制約がRustの安全性や高速性を実現するための重要な要素となります。以下、それぞれのメモリ領域について詳しく見ていきましょう。

スタック (Stack)

スタックは高速にアクセスできるメモリ領域です。関数が呼び出されるとデータが積まれ(プッシュ)、関数が終了すると自動的にデータが降ろされます(ポップ)。i32のような数値型、bool、固定長の配列など、コンパイル時にサイズが確定しているデータが置かれます。

ヒープ (Heap)

ヒープはスタックと比べると低速ですが、柔軟に利用できるメモリ領域です。プログラムの実行中、任意のタイミングでメモリ領域を確保し、不要になったら解放します。StringVec<T>(可変長配列)など、実行時にサイズが変わる可能性のあるデータが置かれます。

静的領域 (Static Storage)

静的領域はプログラムの実行期間中ずっと存在するメモリ領域です。文字列リテラル(例: "Hello, world!")やグローバル変数など、プログラム全体で共有されるデータが置かれます。static変数もここに配置されます。

TypeScript/JavaScriptではGCがこれらのメモリ管理を自動的に行いますが、Rustでは開発者がこれらを意識して使い分ける必要があるのです。


実例で見るメモリ配置

このスタックとヒープの使い分けが、TypeScriptとRustでどう異なるのか、具体的な例で見ていきましょう。

TypeScriptの場合

まず、TypeScriptでコードを書く際のメモリ管理を考えてみましょう。

// 文字列
const message = "Hello, world!";
let greeting = "Hello";
greeting += ", world!"; // 文字列の連結

// 配列
const numbers = [1, 2, 3];
numbers.push(4); // 要素の追加

TypeScript/JavaScriptでは、このコードを書くとき、文字列がどのメモリ領域に保存されるかを意識することはありません。JavaScriptエンジンが自動的に最適な場所にメモリを割り振ってくれます。

Rustの場合 - 明示的なメモリ管理による安全性

一方、Rustの場合を確認してみましょう。Rustでは、文字列と配列に対して2つの異なる型が用意されており、それぞれが異なるメモリ領域に保存されます。

1. 文字列: &strString

TypeScriptでは文字列は string型で扱いますが、Rustには主に2種類の文字列型があります。

&str (文字列スライス / 文字列リテラル):

  let s1: &str = "Hello, world!";

&strは参照型(データそのものではなく、「データのありかを示す情報(アドレス)」を持つ型のこと)なので、「スタック」上に変数 s1が格納され、s1が「文字列データへのポインタ」と「長さ」という2つの情報を持ちます。実際の文字列データ("Hello, world!")自体は「静的領域」に保存されます。s1はそれを指し示しているだけです。代入時には、このポインタと長さの情報がコピーされます(参照のコピー)。本体が読み取り専用なので、&strの中身を後から変更することはできません。

String (文字列型)

一方で、String型は後から文字を追加したり変更したりできる動的な文字列です。

  let mut s2: String = String::from("Hello");

この文字列の実際のデータ ("Hello") は「ヒープ領域」に確保されます。そして s2という変数は、そのヒープデータへのポインタや現在の長さ、確保済みの容量といった管理情報を「スタック」上に保持します。ヒープにあるからこそ、s2.push_str(", world!");のように後からデータを追加できるのです。

&strとの違いは、実際の文字列データが「静的領域」ではなく「ヒープ領域」に配置される点です。ヒープは動的に確保・解放できるため、文字列の長さを後から変更できます。

2. 配列: [T; N] vs Vec<T>

配列も同様です。

  • 配列 [i32; 3]:
  let mut a: [i32; 3] = [1, 2, 3];
  a[0] = 10; // OK! 配列の個々の要素は変更できる
  // a.push(4); // コンパイルエラー! 要素の追加はできない

[i32; 3]は「i32型の要素が3つ」という固定長の配列です。サイズがコンパイル時に確定しているため、このデータは丸ごとスタック上に確保されます。

重要なポイント:

  • スタックに保存されているため、配列のサイズ変更(要素の追加・削除)はできません
  • しかし、配列の各要素の値は変更できます(mutが必要)
  • これは、スタック上の固定サイズ領域内で値を書き換えるだけなので、メモリレイアウトに影響しないためです
  • ベクタ Vec<i32>:
  let mut v: Vec<i32> = vec![1, 2, 3];
  v[0] = 10;  // OK! 要素の値を変更
  v.push(4);  // OK! 要素の追加
  v.pop();    // OK! 要素の削除

Vec<T>は可変長の配列で、TypeScriptの Arrayに近いです。実際のデータはヒープ領域に確保され、vStringと同様に管理情報をスタックに持ちます。

重要なポイント:

  • ヒープに保存されているため、配列のサイズ変更(要素の追加・削除)が可能
  • もちろん、各要素の値も変更できます
  • ヒープ領域は動的に伸縮できるため、容量が足りなくなれば、より大きなメモリ領域を確保し直してデータを再配置します

ここまで見てきたように、TypeScriptでは string 1つ、Array<T> 1つで済むのに対し、Rustでは &strString[T; N]Vec<T> という2つの型を使い分ける必要があります。


なぜRustはスタックとヒープを使い分けるのか?

ここで自然な疑問が浮かびます。「そもそも、全部ヒープに保存すれば、型を2つに分ける必要はないのでは?」

実際、TypeScript/JavaScriptはまさにそのアプローチを取っており、GCのある言語では基本的にほとんどのデータをヒープに置いて管理しています。しかし、Rustがスタックとヒープを使い分けるのには、明確な理由があります。

1. パフォーマンス - スタックは高速

スタックとヒープでは、メモリの確保・解放にかかる時間に大きな違いがあります。

スタックは高速にアクセスできるメモリ領域です。メモリの確保と解放はポインタの移動だけで完了するため、数ナノ秒で済みます。

一方、ヒープはメモリアロケータと呼ばれるヒープメモリを管理するシステムが、メモリの中から適切な空き領域を探索し、確保するのに時間がかかります。

ヒープ領域の容量は、ヒープ上に「とりあえずこれだけ確保しておいた」メモリ全体のサイズです。上の String型の図では容量を5バイト確保しています。例えば、pushなどでそれに対して文字を追加すると確保した容量5バイトでは足りなくなるケースがあります。

この場合、もっと大きなメモリをヒープに確保する必要があり、この作業を再確保(リアロケーション)と呼びます。リアロケーションは新しいメモリ領域を確保し、既存のデータをコピーし、古いメモリを解放するという一連の操作が必要になるため、非常にコストが高くなり、その分動作が遅くなるのです。

2. メモリ効率 - 管理コストが無駄

ヒープにデータを配置する場合、実際のデータサイズに加えて、メモリ管理のためのオーバーヘッドが必要になります。

例えば、4バイトの整数をヒープに配置すると、データ本体の4バイトに加えて、メモリアロケータが管理情報として16〜32バイトのオーバーヘッドを必要とします。

一方、スタックに配置する場合、このような管理オーバーヘッドは一切不要です。4バイトの整数は、そのまま4バイトのメモリ領域だけで済み、メモリ効率は100%です。

全てをヒープに配置すると、本来必要のない管理コストで大量のメモリを浪費することになります。

3. コンパイル時最適化 - コンパイラが賢く最適化できる

スタック上のデータは、コンパイラが最適化を行います。

例えば、コンパイルする時に常に同じ結果になることがわかれば、その計算を定数に置き換えることができます。 以下のような、スタック上の変数だけを使う単純な関数を考えます。

fn add_on_stack() -> i32 {
    let a: i32 = 2;
    let b: i32 = 3;
    a + b
}

この関数は、常に 5 を返すことがコンパイル時にわかるため、コンパイラはこの関数を単に 5 を返すコードに置き換えることができます。これにより、わざわざ実行時に関数を呼び出して、スタックに abのメモリを確保して計算する必要がなくなります。

一方、ヒープへのメモリ確保は「副作用」と見なされるため、コンパイラは簡単に省略できません。たとえ計算結果が常に同じであっても、メモリ確保の操作は保持される必要があります。このため、ヒープを使う場合は最適化の余地が制限され、実行時のパフォーマンスを阻害する可能性があるのです。

スタックを活用することで、コンパイラはより積極的な最適化を行い、実行時のコードサイズと実行速度の両方を改善できるのです。

4. データサイズによる使い分け - 巨大なデータはヒープが有利

ここまでスタックの利点を説明してきましたが、常にスタックが最適というわけではありません。

データサイズが大きい場合、スタックに配置するとコピーコストが問題になります。例えば、数メガバイトの画像データや大きな構造体を関数間で受け渡す場合、スタック上でそのまま扱うと、関数呼び出しのたびにデータ全体がコピーされてしまいまい、パフォーマンスが低下する可能性があります。

// 巨大な構造体の例
struct LargeData {
    buffer: [u8; 1_000_000], // 1MBの配列
}

fn process_data(data: LargeData) {
    // data全体(1MB)がコピーされる
}

一方、ヒープにデータを置き、スタックにはそのポインタだけを持つようにすれば、関数呼び出し時にコピーされるのはポインタだけで済みます。

fn process_data(data: Box<LargeData>) {
    // ポインタ(8バイト)だけがコピーされる
}

このように、Rustでは小さなデータは高速なスタックに、動的なサイズや巨大なデータはヒープにという使い分けが重要になります。


まとめ

ここまで見てきたように、TypeScriptとRustのメモリ管理には大きな違いがあります。

TypeScriptの特徴:

  • ガベージコレクタが自動的にメモリの配置と解放を行う
  • 開発者はメモリを意識せずにコーディングでき、アプリケーションロジックに集中できる
  • しかし、GCの動作タイミングは予測不可能で、パフォーマンスへの影響や意図しないメモリリークの可能性がある
  • メモリの仕組みを理解していないと、クロージャによる意図しない参照保持や、大きなオブジェクトの不要なコピーなど、気づかないうちにパフォーマンス上の問題を引き起こす可能性がある

Rustの特徴:

  • 型システムを通じて、データをどこに配置するかを明示的に制御する
  • &strは静的領域、Stringはヒープ、[T; N]はスタック、Vec<T>はヒープ、というように配置場所を明確に意識して使い分ける
  • コンパイル時にメモリ配置が確定し、メモリ解放のタイミングも確定的

TypeScriptはメモリを意識せず開発ができる反面、不要になった値がいつ解放されるか分からず、メモリリークを引き起こす可能性は否定できません。

一方でRustはメモリ管理を明示的に行う必要がありますが、その分パフォーマンスや安全性を高いレベルで保証できます。

どちらが優れているという話ではありませんが、Rustの仕組みを学ぶことで、TypeScriptでは意識しなかったメモリや、型システムの本質に気づくことができます。

このブログを通して、Rustのメモリ管理の仕組みを理解することで、普段は意識していなかったメモリの扱い方や、GCに任せることのトレードオフに気づき、さらなる技術的洞察を得られることを願っています。

皆様もこの機会に、ぜひRustの世界にも触れてみてはいかがでしょうか?

スパイダープラスでは、エンジニアを募集しています。興味のある方はぜひ、カジュアル面談からでもお気軽にご応募ください!

TypeScriptだけでなくRustの経験者の方も大歓迎です。

なぜ今、基礎資格?QA歴10年弱の私が、あえてJSTQB FLに挑戦する理由

こんにちは。プロダクト品質部の後藤です。

最近、「JSTQB認定テスト技術者資格 Foundation Level」の取得に向けて学習を始めました。

今回は、私がなぜこの資格を受験しようと思ったのか、現在どのような風に学習を進めているかについて書こうと思います。ちなみに、試験はまだ受けていません。

JSTQB認定テスト技術者資格 Foundation Levelってこんな資格

JSTQB認定テスト技術者資格」は、ソフトウェアテストに関する知識とスキルを認定する、日本国内における実質的な標準資格です。

続きを読む

「ユーザーからの"なぜか●●"を紐解く!CREの不具合調査日誌」

はじめまして。 スパイダープラス CREの森川です。

24歳でIT業界に入り、出産を経てからは、システム保守やテクニカルサポートといった分野で経験を積んできました。そんな私の最近一番の難題は、仕事のトラブルシュートより難しい、思春期の子どもとのコミュニケーションです。

さて、私の所属するCREチームは、お客様が直面する技術的な課題を解決し、サービスの安定稼働を支えるミッションを元に日々活動しています。メンバーがそれぞれ異なる強みを持ってるため、一人では解決が困難な問題も、チームとして連携することで乗り越える場面が多々あります。このような『チームへ貢献する意識』がその先にいるお客様の課題を解決したいという、自然なモチベーションの源泉になっていると感じています。

続きを読む

「アジャイル、始めました」がゴールになってない?形骸化しないスクラムのための、はじめの一歩

こんにちは。スパイダープラスで開発チームのEMを担当している細矢です。

近年、ソフトウェア開発においてアジャイル開発の考え方が浸透し、「うちのチームもアジャイルです!」という言葉を耳にする機会が増えました。

とはいえ、「これから導入を検討している」というチームや、「始めてはみたものの、本当に上手くいっているのだろうか?」と悩んでいるチームも、まだまだ多いのではないでしょうか。

この記事では、そういった方々に向けて、弊社での最近の取り組みをご紹介します。アジャイル開発をこれから始める方、そして既に始めているけれどもしっくりきていないと感じている方にとって、「はじめの一歩」を踏み出すヒントになれば幸いです。

この記事はこんな人におすすめです

  • これからアジャイル開発やスクラムを始めようとしている方
  • スクラムを導入してはみたものの、なんだか形骸化していると感じている方
  • チームの目的意識を再確認し、パフォーマンスを向上させたいと考えているマネージャーやリーダーの方
続きを読む