RustGo:ほぼゼロのオーバーヘッドでGoからRustを呼び出す

Goはアセンブラー関数の呼び出しを適切にサポートしており、標準ライブラリの大量の非常に高速な暗号化コードは、実際には、20倍以上の速度を提供する最適化されたアセンブラーです。







しかし、アセンブラーコードの記述は依然として困難であり、それを分析することはさらに難しく、暗号化は間違いを許しません 。 これらの関数をより高いレベルの言語で書くことができたら素晴らしいと思いませんか?







この投稿は、アセンブラーの呼び出しと比較できるほど高速に実行しようとして、GoからRustコードを呼び出すためのわずかに不適切な実験についてです。 Rustやコンパイラの内部を知る必要はなく、リンカとは何かを理解するだけです。







なぜ錆びたのですか?



私はすぐに認めます-私はRustを知りません、そして、それについて書くという考えは私にあまり魅力的ではありません。 それでも、Rustは非常によく調整され最適化された言語であり、アセンブラーよりも読みやすいことを知っています。 (最終的には、すべてがアセンブラよりも読みやすくなります!)







Goでは、通常、デフォルト値はメインタスクに合うように選択され、デフォルトでは高速であることが保証されている機能のみが含まれます-これは、多くのパラメータを必要とする絶え間なく成功する闘争に役立ちます。 私は彼が大好きです。 しかし、その後、今日行うことのために、無効化されたセキュリティチェックでスタックのみの関数を生成するように要求した場合に、瞬きしない言語が必要になります。







そして、アセンブラーのように振る舞うために十分に制限し、アセンブラーと同じくらい効率的になるように最適化できるような言語がある場合、それはおそらくRustになります。







最終的に、Rustは安全で、積極的に開発されており、重要なこととして、使用可能な高速暗号化コードの優れたエコシステムを既に持っています。







なぜcgoではありませんか?



Goには、 Foreign Function Interfaceがあり 、そのまま使用できます。 cgoを使用すると、Goプログラムは最も自然な方法でC関数を呼び出すことができます。残念ながら、これはまったく自然なことではありません。 (私はcgoについて私が望んでいるよりも多くのことを知っています、そして、私を信じて、これは全く面白くないです)。







C ABIをFFIの「lingua franca」として使用すると、どこからでも呼び出すことができます。RustはC ABIと互換性のあるライブラリにコンパイルでき、cgoはそれを使用できます。 これは愚かですが、動作します。







逆に、GoをCライブラリにコンパイルして、さまざまな言語から呼び出すこともできます。たとえば、私はPythonをトリックとして実行しました 。 (人々、それは単なるトリックでした、真剣に受け取らないでください)







しかし、cgoはGoの自然さを少し追加するために内部で多くのことを行います.Cコードのスタックを編成し、Go呼び出しでパニックが発生した場合に正しく動作するように延期呼び出しをセットアップします...これについては別の投稿を書くことができます。







しかし、その結果、各cgo呼び出しのコストは、今日お話ししているような、軽快な小さな関数の場合には高すぎます







すべてを一緒に編む



一般に、この考え:アセンブラー、Rustのコードなどの分離コードがある場合、理論的には、アセンブラー同様にそれを使用て直接呼び出すことができるはずです。 薄い層でもかまいません。







中程度のIRレベルで作業することは望ましくありません。Goコンパイラーは、 Go 1.3から始めても喜ぶ前に、Goコードとアセンブラーの両方をマシンコードに変換します。







これは、システムリンカーを使用してGoでプログラムをリンクするときに「外部リンク」などの概念が存在することで確認されます。 これは、cgoの動作とまったく同じです。最初に、CはCコンパイラ、Go-Goコンパイラでコンパイルされ、これらはすべてclang



またはgcc



を使用してリンクされます。 CGO_LDFLAGS



を介して、戦艦にフラグを直接渡すこともできます。







cgoのすべてのセキュリティ対策の裏では、もちろん、最終的には多言語の課題自体が見つかります。







しかし、もちろん、コンパイラを変更せずにこれを行う方法を見つけることは素晴らしいことです。 まず、GoプログラムをRustアーカイブにリンクする方法を見てみましょう。







#cgo



ディレクティブを使用する場合を除き、 go build



を使用して外部バイナリにリンクする通常の方法を見つけることができませんでした(その理由は?)。 ただし、cgo呼び出しは.sファイルを作成します。このファイルは、GoではなくCコンパイラに渡されます。つまり、Goアセンブラが必要であることを意味します。







幸いなことに、 go / buildは単なるフロントエンドです! Goは、プログラムのコンパイルリンクのための一連の低レベルユーティリティを提供します。gobuildは、ファイルを収集して束にし、これらのユーティリティを実行go build



だけです。 -x



フラグで何が起こるかを追跡できます。







-x -ldflags "-v -linkmode=external '-extldflags=-v'"



するときに-x -ldflags "-v -linkmode=external '-extldflags=-v'"



呼び出しに似た小さなMakefileを作成しました。







 rustgo: rustgo.a go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v rustgo.a rustgo.a: hello.go hello.o go tool compile -o rustgo.a -p main -buildid b01dca11ab1e -pack hello.go go tool pack r rustgo.a hello.o hello.o: hello.s go tool asm -I "$(shell go env GOROOT)/pkg/include" -D GOOS_darwin -D GOARCH_amd64 -o hello.o hello.s
      
      





これにより、1つのGoファイル( hello.go



)とアセンブラーGoファイル( hello.s



)で構成される単純なメインパッケージが作成されます。







さて、オブジェクトをRustにリンクしたい場合、まず静的ライブラリとしてビルドする必要があります...







 libhello.a: hello.rs rustc -g -O --crate-type staticlib hello.rs
      
      





...そして、外部リンカにそれらをリンクするように指示します。







 rustgo: rustgo.a libhello.a go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v -extldflags='-lhello -L"$(CURDIR)"' rustgo.a
      
      





 $ make go tool asm -I "/usr/local/Cellar/go/1.8.1_1/libexec/pkg/include" -D GOOS_darwin -D GOARCH_amd64 -o hello.o hello.s go tool compile -o rustgo.a -p main -buildid b01dca11ab1e -pack hello.go go tool pack r rustgo.a hello.o rustc --crate-type staticlib hello.rs note: link against the following native artifacts when linking against this static library note: the order and any duplication can be significant on some platforms, and so may need to be preserved note: library: System note: library: c note: library: m go tool link -o rustgo -extld clang -buildmode exe -buildid b01dca11ab1e -linkmode external -v -extldflags="-lhello -L/Users/filippo/code/misc/rustgo" rustgo.a HEADER = -H1 -T0x1001000 -D0x0 -R0x1000 searching for runtime.a in /usr/local/Cellar/go/1.8.1_1/libexec/pkg/darwin_amd64/runtime.a searching for runtime/cgo.a in /usr/local/Cellar/go/1.8.1_1/libexec/pkg/darwin_amd64/runtime/cgo.a 0.00 deadcode 0.00 pclntab=166785 bytes, funcdata total 17079 bytes 0.01 dodata 0.01 symsize = 0 0.01 symsize = 0 0.01 reloc 0.01 dwarf 0.02 symsize = 0 0.02 reloc 0.02 asmb 0.02 codeblk 0.03 datblk 0.03 sym 0.03 headr 0.06 host link: "clang" "-m64" "-gdwarf-2" "-Wl,-headerpad,1144" "-Wl,-no_pie" "-Wl,-pagezero_size,4000000" "-o" "rustgo" "-Qunused-arguments" "/var/folders/ry/v14gg02d0y9cb2w9809hf6ch0000gn/T/go-link-412633279/go.o" "/var/folders/ry/v14gg02d0y9cb2w9809hf6ch0000gn/T/go-link-412633279/000000.o" "-g" "-O2" "-lpthread" "-lhello" "-L/Users/filippo/code/misc/rustgo" 0.34 cpu time 12641 symbols 5764 liveness data
      
      





Rustへ



リンクしましたが、シンボル自体は、単にバイナリファイルで隣り合って座っているだけでは何もできません。 GoコードからRust関数を何らかの方法で呼び出す必要があります。







GoからGo関数を呼び出す方法は既に知っています。 アセンブラーでは、この呼び出しはCALL hello(SB)



ようになります。ここで、SBはすべてのグローバル文字でアクセス可能な仮想レジスタです。







Goからアセンブラーで関数を呼び出す場合、コンパイラーにそのことを知らせる必要があります-Cヘッダーのようなもので、関数の本体なしでfunc hello()



のみを記述します。







外部Rust関数の呼び出しの上記の組み合わせをすべて試しましたが、関数のシンボルまたは本体が表示されないという不満がありました。







しかし、最終的には単なる大きなコードジェネレーターであるcgoは、どういうわけかこのエイリアン関数を呼び出すことができます! しかし、どのように?







私は数日後に答えに出会いまし







 //go:cgo_import_static _cgoPREFIX_Cfunc__Cmalloc //go:linkname __cgofn__cgoPREFIX_Cfunc__Cmalloc _cgoPREFIX_Cfunc__Cmalloc var __cgofn__cgoPREFIX_Cfunc__Cmalloc byte var _cgoPREFIX_Cfunc__Cmalloc = unsafe.Pointer(&__cgofn__cgoPREFIX_Cfunc__Cmalloc)
      
      





面白いプラグマのように見えます! //go:linkname



は、ローカルスコープ( プライベート関数を呼び出すために使用できる! )内の文字のエイリアスを作成するだけです。ここでのbyte



トリックは、ある種のアドレス操作のためだけであると確信していbyte



//go:cgo_import_static



...外字をインポートします!







この新しい知識と上記のMakefileを使用して、Rust(hello.rs)から関数を呼び出すことができます。







 #[no_mangle] pub extern fn hello() { println!("Hello, Rust!"); }
      
      





(マングル/パブ/エクスターンのない魔術は、 このチュートリアルから取られています







そして、Go( hello.go



)でこのプログラムから呼び出します:







 package main //go:cgo_import_static hello func trampoline() func main() { println("Hello, Go!") trampoline() }
      
      





このアセンブラーの例を使用( hello.s



):







 TEXT ·trampoline(SB), 0, $2048 JMP hello(SB) RET
      
      





CALL



は洗練されすぎて使用できませんでしたが、単純なJMP



を使用して...







 Hello, Go! Hello, Rust! panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x0]
      
      





さて、終了しようとしたときにプログラムがクラッシュしました。 そして今、この$2048



値はRustが与えたスタック全体であり(スタックをあるべき場所に置く場合)、Rustが少なくともヒープに触れようとした場合に何が起こるかは尋ねません...しかし、いまいましい、私はそれがまったく機能することに驚いています!







呼び出し規約



プログラムをきれいに終了し、ある種の引数を渡したい場合は、Go and Rustの呼び出し規約をさらに詳しく調べる必要があります。 これらの規則により、関数呼び出し間で引数と戻り値が配置される場所が決まります。







Goの呼び出し規則については、 ここここで説明します 。 Rustの場合、 FFI標準を調べる必要がありますが、これはCの標準的な合意にすぎません。







続行するには、デバッガが必要です。 (LLDBはGoをサポートしていますが、MacOS Xではブレークポイントが失敗するため、特権Dockerコンテナー内でこれを行う必要がありました)













通話契約









Goの呼び出し規約はほとんど文書化されていませんが、さらに進むにはそれを理解する必要があるため、この逆アセンブラーリスト(amd64)から学ぶことができます。 非常に単純な関数を見てみましょう:







 // func foo(x, y uint64) uint64 TEXT ·foo(SB), 0, $256-24 MOVQ x+0(FP), DX MOVQ DX, ret+16(FP) RET
      
      





foo



は256(0x100)バイトのローカルフレーム、16バイトの引数、8バイトの戻り値があり、最初の引数のみを返します。







 func main() { foo(0xf0f0f0f0f0f0f0f0, 0x5555555555555555)
      
      





 rustgo[0x49d785]: movabsq $-0xf0f0f0f0f0f0f10, %rax rustgo[0x49d78f]: movq %rax, (%rsp) rustgo[0x49d793]: movabsq $0x5555555555555555, %rax rustgo[0x49d79d]: movq %rax, 0x8(%rsp) rustgo[0x49d7a2]: callq 0x49d8a0 ; main.foo at hello.s:14
      
      





上記の呼び出しコードはほとんど何もしません:フレームの下部にある逆の順序でスタックに引数を置き( rsp



から16(rsp)



に、スタックが大きくなることを忘れないでください)、 CALL



を呼び出します。 CALL



呼び出すと、戻り値へのポインターがスタックにプッシュされ、ジャンプします。 ここには呼び出し関数のクリアはなく、最後に単純なRET



があります。







rsp



修正されていることに注意してください。ここではmovq



代わりにmovq



があります







 rustgo`main.foo at hello.s:14: rustgo[0x49d8a0]: movq %fs:-0x8, %rcx rustgo[0x49d8a9]: leaq -0x88(%rsp), %rax rustgo[0x49d8b1]: cmpq 0x10(%rcx), %rax rustgo[0x49d8b5]: jbe 0x49d8ee ; main.foo + 78 at hello.s:14 [...] rustgo[0x49d8ee]: callq 0x495d10 ; runtime.morestack_noctxt at asm_amd64.s:405 rustgo[0x49d8f3]: jmp 0x49d8a0 ; main.foo at hello.s:14
      
      





最初の4つの関数命令と最後の2つの関数命令は、スタックに十分なスペースがあるかどうかをチェックし、スペースがない場合は、 runtime.morestack



を呼び出しruntime.morestack



NOSPLIT



とラベル付けされた関数では、それらはおそらくスキップされます:







 rustgo[0x49d8b7]: subq $0x108, %rsp [...] rustgo[0x49d8e6]: addq $0x108, %rsp rustgo[0x49d8ed]: retq
      
      





次に、 rsp



コントロールを使用します。このコントロールでは、0x108が減算され、一度にフレーム用の0x100バイトとポインター用の8バイトのスペースが解放されます。 その結果、 rsp



は関数フレームの下部(終了)を指し、呼び出された関数によって制御されます。 戻る前に、 rsp



は元の場所(リターンポインタの直後)に戻ります。







 rustgo[0x49d8be]: movq %rbp, 0x100(%rsp) rustgo[0x49d8c6]: leaq 0x100(%rsp), %rbp [...] rustgo[0x49d8de]: movq 0x100(%rsp), %rbp
      
      





最後に、フレームへのポインターは、本質的に、リターンポインターの直後にスタックにrbp



れ、 rbp



で更新されrbp



rbp



も呼び出された関数によって保存され、スタックをスピンできるようにするために、呼び出された関数がrbp



を保存した場所で更新する必要があることがrbp



ました。







 rustgo[0x49d8ce]: movq 0x110(%rsp), %rdx rustgo[0x49d8d6]: movq %rdx, 0x120(%rsp)
      
      





その結果、関数本体から、戻り値が引数のすぐ上にあることがわかりました。







仮想レジスタ



Goのドキュメントでは、 SP



FP



rsp



rbp



エイリアスではなく、仮想レジスタであるとrbp



ます。







GoアセンブラからSP



を使用すると、 SP



がフレームの下部ではなく上部を指すように、すべてのオフセットが実際のレジスタrsp



に対して再計算されることは明らかです。 これは、フレームのサイズが変更されたときにすべてのオフセットを変更できない可能性があることを意味するため、便利ですが、これは実際には単なる構文上のシュガーです。 ベアレジストリアクセス( MOV SP, DX



)は、 rsp



直接アクセスします。







仮想FP



レジスタもrsp



関連して再rsp



ます。 これは、呼び出し側の関数のフレームの一番下を指し、引数があり、直接アクセスできません。







注:Goはrbp



およびフレームポインターを保存してデバッグを支援しますが、仮想FP



rsp



およびrsp



固定omit-stack-pointer



スタイルomit-stack-pointer



を固定して使用しomit-stack-pointer



。 フレームへのポインターと、アダムラングレーによるこの投稿での使用方法について詳しく読むことができます。







Cの呼び出し規約



Cでは、標準のx86-64呼び出し規約はsysv64



であり、まったく異なります。









rustc



も同じように機能します( -g



とともにrustc



を使用して生成されます)。







すべてをまとめる



2つの契約の間に単純な踏み台を作成することは難しくありません。 インスピレーションについてはasmcgocall



を見ることができます。それはasmcgocall



についてのみです。







Goがアセンブラ関数のスタックを使用するようにしたいのは覚えておく必要があります。Goが存在することを保証しているからです。 これを行うには、スタックの最後にrsp



を返す必要があります。







 package main //go:cgo_import_static increment func trampoline(arg uint64) uint64 func main() { println(trampoline(41)) }
      
      





 TEXT ·trampoline(SB), 0, $2048-16 MOVQ arg+0(FP), DI // Load the argument before messing with SP MOVQ SP, BX // Save SP in a callee-saved registry ADDQ $2048, SP // Rollback SP to reuse this function's frame ANDQ $~15, SP // Align the stack to 16-bytes CALL increment(SB) MOVQ BX, SP // Restore SP MOVQ AX, ret+8(FP) // Place the return value on the stack RET
      
      





 #[no_mangle] pub extern fn increment(a: u64) -> u64 { return a + 1; }
      
      





macOSでの呼び出し



実際、 CALL



はmacOSとあまり仲が良くありません。 何らかの理由で、関数呼び出しがcgo_thread_start



中間呼び出しに置き換えられました。これは、 cgo_import_static



と呼ばれるものを使用し、GoアセンブラーでもCALL



cgo_import_static



いることを考えると、それほど奇妙ではありません。







 callq 0x40a27cd ; x_cgo_thread_start + 29
      
      





//go:linkname



を使用してこの「ヘルプ」を回避できます。これは、標準ライブラリで関数へのポインタを取得し、次のように関数ポインタを呼び出します。







 import _ "unsafe" //go:cgo_import_static increment //go:linkname increment increment var increment uintptr var _increment = &increment
      
      





  MOVQ ·_increment(SB), AX CALL AX
      
      





速いですか?



この実験の全タスクは、暗号操作のためにアセンブラーの代わりにRustを呼び出すことでした(楽しんでください)。 したがって、rustgo呼び出しは、アセンブラー呼び出しと同じくらい高速である必要があります。







ベンチマーク時間!







インラインバージョンでのuint64変数の増加を、 //go:noinline



ディレクティブ、rustgo呼び出し、およびRust関数自体のcgo呼び出しと比較します。







Rustは-g -O



フラグを使用してコンパイルされ、ベンチマークは2.9GHz Intel Code i5プロセッサー上のmacOSで実行されました。







 name time/op CallOverhead/Inline 1.72ns ± 3% CallOverhead/Go 4.60ns ± 2% CallOverhead/rustgo 5.11ns ± 4% CallOverhead/cgo 73.6ns ± 0%
      
      





rustgoは、通常のGo関数を呼び出すよりも11%遅く、cgoよりもほぼ15倍高速です!







Linuxではポインターの問題がなく、結果はさらに良くなり、2%だけ遅くなります。







 name time/op CallOverhead/Inline 1.67ns ± 2% CallOverhead/Go 4.49ns ± 3% CallOverhead/rustgo 4.58ns ± 3% CallOverhead/cgo 69.4ns ± 0%
      
      





実際の例



実際の例として、すばらしいライブラリcurve25519-dalekを選択しました。具体的には、曲線の開始点にスカラーを掛けて、そのEdwards表現を返すタスクを選択しました。







貨物のベンチマークは、プロセッサの周波数動的に変化するため、打ち上げ間で大きく異なりますが、操作が22.9µs±17%を占めるとおおよそ約束します。







 test curve::bench::basepoint_mult ... bench: 17,276 ns/iter (+/- 3,057) test curve::bench::edwards_compress ... bench: 5,633 ns/iter (+/- 858)
      
      





Go側では、単純なAPIを追加します。







 func ScalarBaseMult(dst, in *[32]byte)
      
      





Rust側では、これは通常のFFIのインターフェイスを構築することと大差ありません。







率直に言って、Rustでこれを機能させるには永遠に時間がかかりました。







 #![no_std] extern crate curve25519_dalek; use curve25519_dalek::scalar::Scalar; use curve25519_dalek::constants; #[no_mangle] pub extern fn scalar_base_mult(dst: &mut [u8; 32], k: &[u8; 32]) { let res = &constants::ED25519_BASEPOINT_TABLE * &Scalar(*k); dst.clone_from(res.compress_edwards().as_bytes()); }
      
      





.a



を作成するには、 Cargo.toml



cargo build --release



Cargo.toml



で実行しCargo.toml



これは、依存関係を示し、フレームへのポインターを含み、標準ライブラリなしで最も高度な数学を使用するようにcurve25519-dalekを構成します。







 [package] name = "ed25519-dalek-rustgo" version = "0.0.0" [lib] crate-type = ["staticlib"] [dependencies.curve25519-dalek] version = "^0.9" default-features = false features = ["nightly"] [profile.release] debug = true
      
      





それでも、スプリングボードを修正して、2つの引数を取り、何も返さないようにする必要があります。







 TEXT ·ScalarBaseMult(SB), 0, $16384-16 MOVQ dst+0(FP), DI MOVQ in+8(FP), SI MOVQ SP, BX ADDQ $16384, SP ANDQ $~15, SP MOVQ ·_scalar_base_mult(SB), AX CALL AX MOVQ BX, SP RET
      
      





その結果、Goからの透過的な呼び出しが行われ、純粋なGoのベンチマークに匹敵する速度で、cgoよりもほぼ6%高速になります。







 name old time/op new time/op delta RustScalarBaseMult 23.7µs ± 1% 22.3µs ± 4% -5.88% (p=0.003 n=5+7)
      
      





比較のために、Goパッケージgithub.com/agl/ed25519/edwards25519



同様の機能-純粋なGo実装では、ほぼ3倍の時間がかかります。







 h := &edwards25519.ExtendedGroupElement{} edwards25519.GeScalarMultBase(h, &k) h.ToBytes(&dst)
      
      





 name time/op GoScalarBaseMult 66.1µs ± 2%
      
      





すべてまとめて梱包する



これで本当にうまくいくことがわかりました! しかし、これを実際に使用できるようにするには、ソリューションは、泥だらけのビルドプロセスを使用してpackage main



強制的に挿入するのではなく、インポートできるパッケージの形式にする必要があります。







そしてここ//go:binary-only-package



が登場します。 この注釈により、ソースコードを無視し、以前に収集した$GOPATH/pkg



.a



ライブラリファイルのみを使用することができます。







ネイティブのGoリンカー( cmd / link内部リンカーとも呼ばれます)で動作する.a



ファイルを構築できれば、 これを配布できます。これにより、ユーザーはクロスコンパイルを含むネイティブコードであるかのようにパッケージをインポートできます (暗黙的にこのプラットフォーム用に.a



を作成します)!







多くの場合、Goはシンプルで、既にアセンブラーとRustのペアがあります。 go doc



で表示できるようにドキュメントを追加することもできます。







 //go:binary-only-package // Package edwards25519 implements operations on an Edwards curve that is // isomorphic to curve25519. // // Crypto operations are implemented by calling directly into the Rust // library curve25519-dalek, without cgo. // // You should not actually be using this. package edwards25519 import _ "unsafe" //go:cgo_import_static scalar_base_mult //go:linkname scalar_base_mult scalar_base_mult var scalar_base_mult uintptr var _scalar_base_mult = &scalar_base_mult // ScalarBaseMult multiplies the scalar in by the curve basepoint, and writes // the compressed Edwards representation of the resulting point to dst. func ScalarBaseMult(dst, in *[32]byte)
      
      





makefileはわずかに変更されます。これは、ライブラリを構築しなくなったためgo tool link



使用を停止できgo tool link









.a



アーカイブ.a



、文字テーブルとともに古代形式のオブジェクト.o



ファイルのコレクションです。 シンボルをlibed25519_dalek_rustgo.a



からlibed25519_dalek_rustgo.a



アーカイブにlibed25519_dalek_rustgo.a



go tool compile



libed25519_dalek_rustgo.a



できるようにすれば、目標を達成できます。







.a



アーカイブは、UNIXユーティリティar



またはGo- cmd / packの内部類似物( go tool pack



)で動作します。 もちろん、2つの形式は非常にわずかに異なります。 libed25519_dalek_rustgo.a



にはar



libed25519_dalek_rustgo.a



にはcmd/pack



を使用する必要があります。







(たとえば、私のmacOSのar



は、 BSD規則を使用して#1/LEN



ファイルを呼び出し、このファイルの先頭に長さLENのファイル名を挿入して、最大ファイル長の16バイトをバイパスします。これは紛らわしいです。)







これらの2つのライブラリをリンクするために、最も単純な(read:crutch)メソッドを作成しようとしましたlibed25519_dalek_rustgo.a



を別のフォルダーに抽出し、そのオブジェクトをedwards25519.a



ます。







 edwards25519/edwards25519.a: edwards25519/rustgo.go edwards25519/rustgo.o target/release/libed25519_dalek_rustgo.a go tool compile -N -l -o $@ -p main -pack edwards25519/rustgo.go go tool pack r $@ edwards25519/rustgo.o # from edwards25519/rustgo.s mkdir -p target/release/libed25519_dalek_rustgo && cd target/release/libed25519_dalek_rustgo && \ rm -f *.o && ar xv "$(CURDIR)/target/release/libed25519_dalek_rustgo.a" go tool pack r $@ target/release/libed25519_dalek_rustgo/*.o .PHONY: install install: edwards25519/edwards25519.a mkdir -p "$(shell go env GOPATH)/pkg/darwin_amd64/$(IMPORT_PATH)/" cp edwards25519/edwards25519.a "$(shell go env GOPATH)/pkg/darwin_amd64/$(IMPORT_PATH)/"
      
      





うまくいったときの驚きを想像してみてください!







.a



ファイルを適切な場所に置いて、このパッケージを使用する簡単なプログラムを作成します。







 package main import ( "bytes" "encoding/hex" "fmt" "testing" "github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519" ) func main() { input, _ := hex.DecodeString("39129b3f7bbd7e17a39679b940018a737fc3bf430fcbc827029e67360aab3707") expected, _ := hex.DecodeString("1cc4789ed5ea69f84ad460941ba0491ff532c1af1fa126733d6c7b62f7ebcbcf") var dst, k [32]byte copy(k[:], input) edwards25519.ScalarBaseMult(&dst, &k) if !bytes.Equal(dst[:], expected) { fmt.Println("rustgo produces a wrong result!") } fmt.Printf("BenchmarkScalarBaseMult\t%v\n", testing.Benchmark(func(b *testing.B) { for i := 0; i < bN; i++ { edwards25519.ScalarBaseMult(&dst, &k) } })) }
      
      





go build



実行しgo build









 $ go build -ldflags '-linkmode external -extldflags -lresolv' $ ./ed25519-dalek-rustgo BenchmarkScalarBaseMult 100000 19914 ns/op
      
      





まあ、それはほとんど働いた。 私は少しポン引きをしなければなりませんでした。 バイナリファイルは、 libresolv



とリンクするまでコンパイルされlibresolv



。 正直に言うと、Rustコンパイラーはそれを言おうとしました。 (しかし、Rustコンパイラが言うことを誰がたまたましますか?)







 note: link against the following native artifacts when linking against this static library note: the order and any duplication can be significant on some platforms, and so may need to be preserved note: library: System note: library: resolv note: library: c note: library: m
      
      





システムライブラリとのリンクは、内部リンカーとクロスコンパイルでは決して起こらないため、問題になります...







しかし、ちょっと、lib resolve? なぜそれがno_std



、「アセンブラのようであるべきだ」、RustライブラリスタックのみがDNS名を解決するために標準ライブラリを使用しようとしていますか?







no_stdと言った



ここでの問題は、ライブラリが実際にはno_std



はないことno_std



。 ここですべてを見てください! アロケーターは必要ありません。







 $ ar t target/release/libed25519_dalek_rustgo.a __.SYMDEF ed25519_dalek_rustgo-742a1d9f1c101d86.0.o ed25519_dalek_rustgo-742a1d9f1c101d86.crate.allocator.o curve25519_dalek-03e3ca0f6d904d88.0.o subtle-cd04b61500f6e56a.0.o std-72653eb2361f5909.0.o panic_unwind-d0b88496572d35a9.0.o unwind-da13b913698118f9.0.o arrayref-2be0c0ff08ae2c7d.0.o digest-f1373d68da35ca45.0.o generic_array-95ca86a62dc11ddc.0.o nodrop-7df18ca19bb4fc21.0.o odds-3bc0ea0bdf8209aa.0.o typenum-a61a9024d805e64e.0.o rand-e0d585156faee9eb.0.o alloc_system-c942637a1f049140.0.o libc-e038d130d15e5dae.0.o alloc-0e789b712308019f.0.o std_unicode-9735142be30abc63.0.o compiler_builtins-8a5da980a34153c7.0.o absvdi2.o absvsi2.o absvti2.o [... snip ...] truncsfhf2.o ucmpdi2.o ucmpti2.o core-9077840c2cc91cbf.0.o
      
      





だから、どのようにすべてのno_std



を行うのでしょうか? これは別の冒険であることが判明しましたが、結論だけを書きます。









lib.rs



:







 #![no_std] #![feature(lang_items, compiler_builtins_lib, core_intrinsics)] use core::intrinsics; #[allow(private_no_mangle_fns)] #[no_mangle] // rust-lang/rust#38281 #[lang = "panic_fmt"] fn panic_fmt() -> ! { unsafe { intrinsics::abort() } } #[lang = "eh_personality"] extern fn eh_personality() {} extern crate compiler_builtins; // rust-lang/rust#43264 extern crate rlibc;
      
      





, go build



(!!!) macOS.







Linux



Linux .







fmax



, , , :







 $ ld -r -o linux.o target/release/libed25519_dalek_rustgo/*.o $ nm -u linux.o U _GLOBAL_OFFSET_TABLE_ U abort U fmax U fmaxf U fmaxl U logb U logbf U logbl U scalbn U scalbnf U scalbnl
      
      





, , --gc-sections



, , . , , ( ):







 $ go build -ldflags '-extld clang -linkmode external -extldflags -Wl,--gc-sections'
      
      





, , Makefile , --gc-sections



? , .a



man- .







.o



, , ld -r --gc-sections -u $SYMBOL



. -r



-u



"", . $SYMBOL



scalar_base_mult



.







macOS? , , , macOS .







 $ ld -e _scalar_base_mult target/release/libed25519_dalek_rustgo/*.o Undefined symbols for architecture x86_64: "___assert_rtn", referenced from: _compilerrt_abort_impl in int_util.o "_copysign", referenced from: ___divdc3 in divdc3.o ___muldc3 in muldc3.o "_copysignf", referenced from: ___divsc3 in divsc3.o ___mulsc3 in mulsc3.o "_copysignl", referenced from: ___divxc3 in divxc3.o ___mulxc3 in mulxc3.o "_fmax", referenced from: ___divdc3 in divdc3.o "_fmaxf", referenced from: ___divsc3 in divsc3.o "_fmaxl", referenced from: ___divxc3 in divxc3.o "_logb", referenced from: ___divdc3 in divdc3.o "_logbf", referenced from: ___divsc3 in divsc3.o "_logbl", referenced from: ___divxc3 in divxc3.o "_scalbn", referenced from: ___divdc3 in divdc3.o "_scalbnf", referenced from: ___divsc3 in divsc3.o "_scalbnl", referenced from: ___divxc3 in divxc3.o ld: symbol(s) not found for inferred architecture x86_64 $ ld -e _scalar_base_mult -dead_strip target/release/libed25519_dalek_rustgo/*.o
      
      





, , macOS _



, .







, Makefile, :]







 edwards25519/edwards25519.a: edwards25519/rustgo.go edwards25519/rustgo.o edwards25519/libed25519_dalek_rustgo.o go tool compile -N -l -o $@ -p main -pack edwards25519/rustgo.go go tool pack r $@ edwards25519/rustgo.o edwards25519/libed25519_dalek_rustgo.o edwards25519/libed25519_dalek_rustgo.o: target/$(TARGET)/release/libed25519_dalek_rustgo.a ifeq ($(shell go env GOOS),darwin) $(LD) -r -o $@ -arch x86_64 -u "_$(SYMBOL)" $^ else $(LD) -r -o $@ --gc-sections -u "$(SYMBOL)" $^ endif
      
      





Linux. , , . CALL



Rust .







, - , rustgo, , , . cmd/link ( !), , Go, , //cgo:cgo_import_static



, //cgo:cgo_import_dynamic



.







 //go:cgo_import_static scalar_base_mult //go:cgo_import_dynamic scalar_base_mult
      
      





, , - rustgo , macOS, Linux, .









, .a



, //go:binary-only-package



tar- .a



linux_amd64/darwin_amd64



, :







 $ tar tf ed25519-dalek-rustgo_go1.8.3.tar.gz src/github.com/FiloSottile/ed25519-dalek-rustgo/ src/github.com/FiloSottile/ed25519-dalek-rustgo/.gitignore src/github.com/FiloSottile/ed25519-dalek-rustgo/Cargo.lock src/github.com/FiloSottile/ed25519-dalek-rustgo/Cargo.toml src/github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519/ src/github.com/FiloSottile/ed25519-dalek-rustgo/main.go src/github.com/FiloSottile/ed25519-dalek-rustgo/Makefile src/github.com/FiloSottile/ed25519-dalek-rustgo/release.sh src/github.com/FiloSottile/ed25519-dalek-rustgo/src/ src/github.com/FiloSottile/ed25519-dalek-rustgo/target.go src/github.com/FiloSottile/ed25519-dalek-rustgo/src/lib.rs src/github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519/rustgo.go src/github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519/rustgo.s pkg/linux_amd64/github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519.a pkg/darwin_amd64/github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519.a
      
      





, , - ( .a



).







, , , Rust -Ctarget-cpu=native



, . ( curve25519-dalek authors ) - Haswell , Haswell:







 $ benchstat bench-none.txt bench-haswell.txt name old time/op new time/op delta ScalarBaseMult/rustgo 22.0µs ± 3% 20.2µs ± 2% -8.41% (p=0.001 n=7+6) $ benchstat bench-haswell.txt bench-native.txt name old time/op new time/op delta ScalarBaseMult/rustgo 20.2µs ± 2% 20.1µs ± 2% ~ (p=0.945 n=6+7)
      
      





, , Makefile GOOS/GOARCH, Rust, Rust -, - .a



.







: github.com/FiloSottile/ed25519-dalek-rustgo/edwards25519

godoc .









, .







, , rustgo , . , , g



, , , , . Rust .







, morestack



NOSPLIT



, , ( rsp



) , , Rust ( ).







, - "rustgo" , Makefile . Cgo , . go:generate



, -, cargo (-, Go Rust!). FFI- Rust, GoSlice



.







 #[repr(C)] struct GoSlice { array: *mut u8, len: i32, cap: i32, }
      
      





または、GoまたはRustの誰かが来て、怪我をする前に立ち止まるように言うかもしれません。







PS。誰かがこれをcgo(セキュリティのためのたくさんの機能を持っている)または純粋なGoと比較し始める前に、rustgoはどちらの代わりにもなりません。これは、手作業で記述されたアセンブラー関数を、同等のパフォーマンスでより安全で読みやすいものに置き換えることを意味していました。さらに良いことに、それは楽しい実験として意図されていました。








All Articles