TL;DR
- Apple Silicon Macで1.12.2 Mod開発をしている場合、Rosetta 2経由でx86_64 JDKを使っているかもしれない
- ただArm64ネイティブなJDKを使うと、
com.mojang.text2speech.NarratorOSXが読み込まれた際にUnsatisfiedLinkErrorが発生してクラッシュするcom.mojang.text2speech.Narrator#getNarratorがNarratorOSXを返すのを阻止するため、 MixinでNarratorDummyを返すようにOverwriteする。 バニラへのMixinなので、IEarlyMixinLoaderを使う。
注意
- 足りないネイティブライブラリを追加するというような、根本的な解決ではない
- Mixinを使用できる環境であることが前提
これは何
Apple Siliconを搭載したMacで、Minecraft 1.12.2に向けたModdingをしている際、
JDKにarm64ネイティブなもの(例えばAzul Zulu Community)を使用すると、ゲーム起動時にUnsatisfiedLinkErrorが出てクラッシュした。
調べると、NarratorOSXが読み込まれることで発生しているので、Mixinを使って強引に回避した。
Crash Report
---- Minecraft Crash Report ----// But it works on my machine.
Time: 10/29/25 6:17 PMDescription: Initializing game
java.lang.UnsatisfiedLinkError: /private/var/folders/t_/t3k2_6sj60q1y9q8dy5b_w040000gn/T/jna-93968178/jna7821772639012253071.tmp: dlopen(/private/var/folders/t_/t3k2_6sj60q1y9q8dy5b_w040000gn/T/jna-93968178/jna7821772639012253071.tmp, 0x0001): tried: '/private/var/folders/t_/t3k2_6sj60q1y9q8dy5b_w040000gn/T/jna-93968178/jna7821772639012253071.tmp' (fat file, but missing compatible architecture (have 'i386,x86_64', need 'arm64e' or 'arm64e.v1' or 'arm64' or 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/private/var/folders/t_/t3k2_6sj60q1y9q8dy5b_w040000gn/T/jna-93968178/jna7821772639012253071.tmp' (no such file), '/private/var/folders/t_/t3k2_6sj60q1y9q8dy5b_w040000gn/T/jna-93968178/jna7821772639012253071.tmp' (fat file, but missing compatible architecture (have 'i386,x86_64', need 'arm64e' or 'arm64e.v1' or 'arm64' or 'arm64'))at java.lang.ClassLoader$NativeLibrary.load(Native Method)at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1950)at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1832)at java.lang.Runtime.load0(Runtime.java:783)at java.lang.System.load(System.java:1100)at com.sun.jna.Native.loadNativeDispatchLibraryFromClasspath(Native.java:947)at com.sun.jna.Native.loadNativeDispatchLibrary(Native.java:922)at com.sun.jna.Native.<clinit>(Native.java:190)at com.sun.jna.Pointer.<clinit>(Pointer.java:54)at ca.weblite.objc.Proxy.<init>(Proxy.java:126)at ca.weblite.objc.Proxy.<init>(Proxy.java:118)at ca.weblite.objc.NSObject.<init>(NSObject.java:112)at ca.weblite.objc.NSObject.<init>(NSObject.java:102)at com.mojang.text2speech.NarratorOSX.<init>(NarratorOSX.java:18)at com.mojang.text2speech.Narrator.getNarrator(Narrator.java:22)at net.minecraft.client.gui.chat.NarratorChatListener.<init>(NarratorChatListener.java:18)at net.minecraft.client.gui.chat.NarratorChatListener.<clinit>(NarratorChatListener.java:17)at net.minecraft.client.gui.GuiIngame.<init>(GuiIngame.java:126)at net.minecraftforge.client.GuiIngameForge.<init>(GuiIngameForge.java:101)at net.minecraft.client.Minecraft.init(Minecraft.java:584)at net.minecraft.client.Minecraft.run(Minecraft.java:422)at net.minecraft.client.main.Main.main(Main.java:118)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.minecraft.launchwrapper.Launch.launch(Launch.java:165)at net.minecraft.launchwrapper.Launch.main(Launch.java:29)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.minecraftforge.gradle.GradleStartCommon.launch(GradleStartCommon.java:87)at GradleStart.main(GradleStart.java:19)
No Mixin Metadata is found in the Stacktrace.
A detailed walkthrough of the error, its code path and all known details is as follows:---------------------------------------------------------------------------------------
-- Head --Thread: Client threadStacktrace:at java.lang.ClassLoader$NativeLibrary.load(Native Method)at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1950)at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1832)at java.lang.Runtime.load0(Runtime.java:783)at java.lang.System.load(System.java:1100)at com.sun.jna.Native.loadNativeDispatchLibraryFromClasspath(Native.java:947)at com.sun.jna.Native.loadNativeDispatchLibrary(Native.java:922)at com.sun.jna.Native.<clinit>(Native.java:190)at com.sun.jna.Pointer.<clinit>(Pointer.java:54)at ca.weblite.objc.Proxy.<init>(Proxy.java:126)at ca.weblite.objc.Proxy.<init>(Proxy.java:118)at ca.weblite.objc.NSObject.<init>(NSObject.java:112)at ca.weblite.objc.NSObject.<init>(NSObject.java:102)at com.mojang.text2speech.NarratorOSX.<init>(NarratorOSX.java:18)at com.mojang.text2speech.Narrator.getNarrator(Narrator.java:22)at net.minecraft.client.gui.chat.NarratorChatListener.<init>(NarratorChatListener.java:18)at net.minecraft.client.gui.chat.NarratorChatListener.<clinit>(NarratorChatListener.java:17)at net.minecraft.client.gui.GuiIngame.<init>(GuiIngame.java:126)at net.minecraftforge.client.GuiIngameForge.<init>(GuiIngameForge.java:101)at net.minecraft.client.Minecraft.init(Minecraft.java:584)
-- Initialization --Details:Stacktrace:at net.minecraft.client.Minecraft.run(Minecraft.java:422)at net.minecraft.client.main.Main.main(Main.java:118)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.minecraft.launchwrapper.Launch.launch(Launch.java:165)at net.minecraft.launchwrapper.Launch.main(Launch.java:29)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.minecraftforge.gradle.GradleStartCommon.launch(GradleStartCommon.java:87)at GradleStart.main(GradleStart.java:19)
-- System Details --Details:Minecraft Version: 1.12.2Operating System: Mac OS X (aarch64) version 26.0.1Java Version: 1.8.0_472, Azul Systems, Inc.Java VM Version: OpenJDK 64-Bit Server VM (mixed mode), Azul Systems, Inc.Memory: 2332871168 bytes (2224 MB) / 2857369600 bytes (2725 MB) up to 5726797824 bytes (5461 MB)JVM Flags: 2 total; -Xms1G -Xmx6GIntCache: cache: 0, tcache: 0, allocated: 0, tallocated: 0FML: MCP 9.42 Powered by Forge 14.23.5.2847 14 mods loaded, 14 mods activeStates: 'U' = Unloaded 'L' = Loaded 'C' = Constructed 'H' = Pre-initialized 'I' = Initialized 'J' = Post-initialized 'A' = Available 'D' = Disabled 'E' = Errored
| State | ID | Version | Source | Signature | |:------ |:------------------- |:------------ |:------------------------------------ |:--------- | | LCHIJA | minecraft | 1.12.2 | minecraft.jar | None | | LCHIJA | mcp | 9.42 | minecraft.jar | None | | LCHIJA | mixinbooter | 10.6 | minecraft.jar | None | | LCHIJA | placeholdername | 1.0.0 | groovyscript-1.2.5-dev.jar | None | | LCHIJA | FML | **.**.**.** | recompiled_minecraft-1.12.2.jar | None | | LCHIJA | forge | 14.23.5.2847 | recompiled_minecraft-1.12.2.jar | None | | LCHIJA | modularui | 2.5.1 | modularui-2.5.1-dev.jar | None | | LCHIJA | codechickenlib | **.**.**.** | codechickenlib-3.2.3.358-dev.jar | None | | LCHIJA | groovyscript | 1.2.5 | groovyscript-1.2.5-dev.jar | None | | LCHIJA | jei | **.**.**.** | jei_1.12.2-4.16.1.302.jar | None | | LCHIJA | theoneprobe | 1.4.28 | top-245211-2667280-deobf.jar | None | | LCHIJA | forgelin_continuous | **.**.**.** | Forgelin-Continuous-2.2.20.0-dev.jar | None | | LCHIJA | clayium | 0.11.0 | clayium-1.12.2-0.11.0-dev.jar | None | | LCHIJA | forgelin | 1.8.4 | Forgelin-Continuous-2.2.20.0-dev.jar | None |
Loaded coremods (and transformers):MixinBooter (mixinbooter-10.6.jar)
ModularUI-Core (modularui-2.5.1-dev.jar)com.cleanroommc.modularui.core.ClassTransformerGroovyScript-Core (groovyscript-1.2.5-dev.jar)
Forgelin-Continuous (Forgelin-Continuous-2.2.20.0-dev.jar)
GL info: ' Vendor: 'Apple' Version: '2.1 Metal - 90.5' Renderer: 'Apple M1 Pro' Launched Version: FML_DEV LWJGL: 2.9.4 OpenGL: Apple M1 Pro GL version 2.1 Metal - 90.5, Apple GL Caps: Using GL 1.3 multitexturing.Using GL 1.3 texture combiners.Using framebuffer objects because ARB_framebuffer_object is supported and separate blending is supported.Shaders are available because OpenGL 2.1 is supported.VBOs are available because OpenGL 1.5 is supported.
Using VBOs: Yes Is Modded: Definitely; Client brand changed to 'fml,forge' Type: Client (map_client.txt) Resource Packs: Current Language: English (US) Profiler Position: N/A (disabled) CPU: 10x Apple M1 Pro扱うもの
- UnsatisfiedLinkErrorの原因
- InterfaceのStatic MethodへのOverwrite Mixin
- Early Mixinに関する設定
- Mixin Plugin
扱わないもの
- Mixin基礎事項
- 一度
ILateMixinLoaderを通じてMixinを利用したことがある前提
- 一度
より詳しい経緯
ある日、いつも通りに1.12.2でModdingしていると、RetroFuturaGradleがこう言ってきた:
[DEPRECATION NOTICE] Support for running Gradle using Java versions older than 21 with the RetroFuturaGradle plugin is scheduled to be removed, please upgrade your local and CI workflow Java version to 21 or newer. This does NOT mean that mod code has to use newer Java, the Gradle process itself will require Java 21 but the compiler and mod code can keep on using Java 8.なので、Gradleを動かすJDKをarm64なJava 21にしたところ、run系タスクがUnsatisfiedLinkErrorでクラッシュして起動できなくなった。
原因
Stacktraceを見ると、Narrator.getNarrator()でクラッシュしているようだ。
Thread: Client threadStacktrace:at java.lang.ClassLoader$NativeLibrary.load(Native Method)... 略 ...at com.mojang.text2speech.NarratorOSX.<init>(NarratorOSX.java:18)at com.mojang.text2speech.Narrator.getNarrator(Narrator.java:22)Narrator.getNarrator()を見ると、OSに対応するNarratorインスタンスを生成している。
MacOSの場合はNarratorOSXが返される。このクラスが存在しないネイティブライブラリを読み込もうとしてクラッシュしている。
static Narrator getNarrator() { final String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); if (osName.contains("linux")) { setJNAPath(":"); return new NarratorLinux(); } else if (osName.contains("win")) { setJNAPath(";"); return new NarratorWindows(); } else if (osName.contains("mac")) { setJNAPath(":"); return new NarratorOSX(); } else { return new NarratorDummy(); }}ナレーターは使っていないので、getNarrator()が単にnew NarratorDummy()を返すようにMixinすれば良さそう。
解決策
開発(deobf)環境でのみ有効なEarly Mixinを作る。
Mixinの作成
Interfaceのstaticメソッドという少し特殊なケースだからなのか、Injectだと動かなかったので、Overwriteする。
package com.example.mixins;
@Mixin(value = Narrator.class, remap = false)public interface MixinNarrator {
/** * @author bqc0n * <p> * @reason NarratorOSX {@link com.mojang.text2speech.NarratorOSX} causes UnsatisfiedLinkError on Apple Silicon Macs * (Maybe native libs are not available in arm64 Java 8 JDKs?). * This mixin should be enabled only on deobf environment. * * @return {@link NarratorDummy} */ @Overwrite(remap = false) static Narrator getNarrator() { return new NarratorDummy(); }}@reasonタグの追加
ここで問題が発生した。
OverwriteするならJavadocに@reasonタグを追加せよと怒られたのだが、追加するとそんなタグはないと怒られてしまう。
このままだとjavadocタスクが通らない。
そこで、build.gradle.ktsを少しいじって@reasonタグを作る。
tasks.withType<Javadoc> { (options as StandardJavadocDocletOptions).addStringOption("tag", "reason:a:\"Reason for Overwrite:\"")}Early Mixinの概要
バニラやforgeへのMixinにはIEarlyMixinLoaderを使う1。
注意点として、IEarlyMixinLoaderを実装するクラスはFMLLoadingPluginも実装する必要がある。
つまりCoreMod化が必要である。
CoreMod設定
CoreMod化の手順は環境によって異なる。
まず前提として、CoreModを作る場合、コードだけではなくMETA-INF/MANIFEST.MFもいじる必要がある。
しかし、CleanroomMC/ForgeDevEnvのようなモダンな1.12.2開発環境では、MANIFEST.MFは自動生成されるようになっている。
実際このテンプレートではgradle.propertiesにCoreMod関連のパラメータが予め用意されている。
もしそう言ったテンプレートを使っていないなら、手でMANIFEST.MFを書く必要がある。 Minecraft Modding Wikiが参考になると思う。
ハマりポイントとして、FMLCorePluginContainsFMLMod: *のような記述をMANIFEST.MFに追加しないと通常のMod部分が読まれない。
つまり@Modアノテーションが付けられたクラスが読み込まれず、CoreMod部分しか動かない。
テンプレートだとcoremod_includes_mod = trueで表現されている。
IEarlyMixinLoaderの実装
今回はMixinを使いたいだけなので、CoreModに関する部分はnullや空配列を返す。
それ以外はILateMixinLoaderと同じ。
@IFMLLoadingPlugin.Name("Example-Core")@IFMLLoadingPlugin.MCVersion(ForgeVersion.mcVersion)public class ExampleCoremod implements IFMLLoadingPlugin, IEarlyMixinLoader { @Override public String[] getASMTransformerClass() { return new String[0]; }
@Override public @Nullable String getModContainerClass() { return null; }
@Override public @Nullable String getSetupClass() { return null; }
@Override public void injectData(Map<String, Object> data) {}
@Override public @Nullable String getAccessTransformerClass() { return null; }
@Override public List<String> getMixinConfigs() { return Collections.singletonList("mixins.core.example.json"); }}ここまできたら、クラッシュ問題は解決しているはず。
ただしこのままだと、開発環境にとどまらず実環境(obf環境)でもMixinが適用されてしまう。
これは避けたいので、IMixinConfigPluginを実装して適用する。
IMixinConfigPluginの実装
IMixinConfigPluginを実装する。
このインターフェースにもgetMixinsがあるが、これはmixin config jsonに書かれた以外のMixinを追加したいときに使うものなので、今回はnullでOK。
重要なのはshouldApplyMixinメソッドで、ここでMixinを適用するかどうかを決定できる。
Mixinクラス名がNarratorで終わり、かつ開発環境でない場合にはfalseを返してMixinが適用されないようにする。
public class ExampleMixinConfigPlugin implements IMixinConfigPlugin { @Override public void onLoad(String mixinPackage) {}
@Override public String getRefMapperConfig() { return ""; }
@Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { //noinspection RedundantIfStatement | I think it is easier to understand if (targetClassName.endsWith("Narrator") && !FMLLaunchHandler.isDeobfuscatedEnvironment()) { return false; } return true; }
@Override public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {}
@Override public List<String> getMixins() { return null; }
@Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}
@Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}}さらに、このMixin Pluginをjsonに書く必要がある。 冗長なので追加部分以外は省略した。
{ "plugin": "example.core.ExampleMixinConfigPlugin",}これで、arm64ネイティブなJDKを使えるようになった。 というか今までRosetta 2経由でx86_64を使っていたのか、通りで微妙に遅かったわけだ…
Footnotes
-
MixinBooterのREADMEを参照: https://github.com/CleanroomMC/MixinBooter ↩