Cara kami mengotomatiskan produk porting dari C # ke C ++

Halo Habr. Dalam posting ini saya akan berbicara tentang bagaimana kami mengatur rilis perpustakaan bulanan untuk bahasa C ++, kode sumbernya dikembangkan dalam C #. Ini bukan tentang C ++ yang dikelola, atau bahkan tentang membuat jembatan antara C ++ yang tidak dikelola dan lingkungan CLR - ini tentang mengotomatisasi pembuatan kode C ++ yang mengulang API dan fungsionalitas kode C # asli.

Kami menulis infrastruktur yang diperlukan untuk menerjemahkan kode antar bahasa dan meniru fungsi perpustakaan .Net sendiri, sehingga memecahkan masalah yang biasanya dianggap akademik. Ini memungkinkan kami untuk mulai merilis rilis bulanan produk-produk pra-Donet untuk bahasa C ++, mendapatkan kode untuk setiap rilis dari versi kode C # yang sesuai. Pada saat yang sama, tes yang mencakup kode asli porting bersama dengannya dan memungkinkan Anda untuk mengontrol kinerja solusi yang dihasilkan bersama dengan tes tertulis khusus di C ++.

Dalam artikel ini saya akan menjelaskan secara singkat sejarah proyek kami dan teknologi yang digunakan di dalamnya. Saya akan menyentuh pada masalah pembenaran ekonomi hanya secara sepintas, karena sisi teknis jauh lebih menarik bagi saya. Dalam artikel seri berikut ini, saya berencana untuk membahas topik-topik seperti pembuatan kode dan manajemen memori, serta beberapa topik lainnya, jika komunitas memiliki pertanyaan yang relevan.

Latar Belakang


Awalnya, perusahaan kami terlibat dalam rilis perpustakaan untuk platform .Net. Perpustakaan ini terutama menyediakan API untuk bekerja dengan beberapa format file (dokumen, tabel, slide, grafik) dan protokol (email), menempati ceruk tertentu di pasar untuk solusi tersebut. Semua pengembangan dilakukan dalam C #.

Pada akhir tahun 2000-an, perusahaan memutuskan untuk memasuki pasar baru untuk dirinya sendiri, mulai merilis produk serupa untuk Jawa. Pengembangan dari awal jelas membutuhkan investasi sumber daya yang sebanding dengan pengembangan awal semua produk yang terkena dampak. Opsi untuk membungkus kode Donnet ke dalam layer yang menerjemahkan panggilan dan data dari Java ke .Net dan sebaliknya juga ditolak karena beberapa alasan. Sebaliknya, pertanyaannya adalah apakah mungkin dengan cara apa pun untuk sepenuhnya memigrasi kode yang ada ke platform baru. Ini semua lebih relevan karena itu bukan promosi satu kali, tetapi rilis bulanan setiap produk baru, disinkronkan antara dua bahasa.

Diputuskan untuk memecah keputusan menjadi dua bagian. Yang pertama - yang disebut Porter - akan mengonversi sintaksis kode sumber C # ke Java, secara bersamaan menggantikan tipe dan metode .Net dengan rekan-rekan mereka dari perpustakaan Java. Yang kedua - Perpustakaan - akan meniru pekerjaan bagian-bagian dari perpustakaan. Net yang sulit atau tidak mungkin untuk membangun korespondensi langsung dengan Jawa, menarik komponen pihak ketiga yang tersedia untuk ini.

Dalam mendukung kelayakan utama dari rencana semacam itu, berikut ini berbicara:

  1. Secara ideologis, bahasa C # dan Java cukup mirip - setidaknya, dengan struktur tipe dan organisasi kerja dengan memori;
  2. Itu tentang porting perpustakaan; tidak perlu port GUI;
  3. , , - , System.Net System.Drawing;
  4. , .Net ( Framework, Standard Xamarin), .

Saya tidak akan merinci, karena mereka pantas mendapatkan artikel terpisah (dan bukan satu). Saya hanya bisa mengatakan bahwa butuh sekitar dua tahun dari awal pengembangan hingga rilis produk Java pertama, dan sejak itu rilis produk Java telah menjadi praktik rutin perusahaan. Selama pengembangan proyek, porter telah berevolusi dari utilitas sederhana yang mengubah teks sesuai aturan yang ditetapkan, menjadi generator kode kompleks yang bekerja dengan representasi AST dari kode sumber. Perpustakaan juga ditumbuhi kode.

Keberhasilan arahan Jawa menentukan keinginan perusahaan untuk memperluas lebih jauh ke pasar baru untuk dirinya sendiri, dan pada tahun 2013 muncul pertanyaan tentang rilis produk untuk bahasa C ++ dalam skenario yang sama.

Perumusan masalah


Untuk memastikan rilis versi positif dari produk, perlu untuk membuat kerangka kerja yang akan memungkinkan Anda untuk mendapatkan kode C ++ dari kode C # yang sewenang-wenang, kompilasi, periksa dan berikan kepada klien. Itu tentang perpustakaan dengan volume mulai dari beberapa ratus ribu hingga beberapa juta baris (tidak termasuk dependensi).

Pada saat yang sama, pengalaman dengan porter Java diperhitungkan: pada awalnya, ketika itu hanya alat sederhana untuk mengkonversi sintaksis, praktik menyelesaikan kode porting secara manual muncul secara alami. Dalam jangka pendek, berfokus pada rilis cepat produk, ini relevan, karena memungkinkan untuk mempercepat proses pengembangan, namun, dalam jangka panjang, ini secara signifikan meningkatkan biaya mempersiapkan setiap versi untuk rilis karena kebutuhan untuk memperbaiki setiap kesalahan terjemahan setiap kali itu terjadi.

Tentu saja, kompleksitas ini dapat dikelola - setidaknya dengan mentransfer hanya tambalan ke dalam kode Java yang dihasilkan, yang dihitung sebagai perbedaan antara output porter untuk dua revisi selanjutnya dari kode C #. Pendekatan ini memungkinkan untuk memperbaiki setiap baris porting hanya sekali dan di masa depan menggunakan kode yang sudah dikembangkan di mana tidak ada perubahan yang dilakukan. Namun, ketika mengembangkan porter positif, tujuannya adalah untuk menyingkirkan tahap memperbaiki kode porting, alih-alih memperbaiki kerangka itu sendiri. Dengan demikian, setiap kesalahan terjemahan yang jarang terjadi akan diperbaiki sekali - dalam kode porter, dan perbaikan ini akan berlaku untuk semua rilis mendatang dari semua produk porting.

Selain porter itu sendiri, itu juga diperlukan untuk mengembangkan perpustakaan di C ++ yang akan memecahkan masalah berikut:

  1. Emulasi lingkungan .Net sejauh diperlukan agar kode porting berfungsi;
  2. Mengadaptasi kode C # yang diangkut ke realitas C ++ (struktur tipe, manajemen memori, kode layanan lainnya);
  3. Menghaluskan perbedaan antara "ditulis ulang C #" dan C ++ itu sendiri, untuk membuatnya lebih mudah bagi programmer yang tidak terbiasa dengan. Net paradigma untuk menggunakan kode porting.

Untuk alasan yang jelas, tidak ada upaya yang dilakukan untuk secara langsung memetakan jenis Net ke jenis dari perpustakaan standar. Sebagai gantinya, diputuskan untuk selalu menggunakan tipe dari perpustakaannya sebagai pengganti untuk tipe Donnet.

Banyak pembaca akan segera bertanya mengapa mereka tidak menggunakan implementasi yang ada seperti Mono . Ada alasan untuk itu.

  1. Dengan menarik perpustakaan yang sudah selesai, hanya mungkin memenuhi persyaratan pertama, tetapi bukan yang kedua dan bukan yang ketiga.
  2. Mono C# , , , .
  3. (API, , , C++, ) , .
  4. , .Net, . , , .

Secara teoritis, pustaka semacam itu dapat diterjemahkan ke dalam C ++ sepenuhnya menggunakan port, namun, ini akan membutuhkan porter yang berfungsi penuh pada awal pengembangan, karena tanpa pustaka sistem debugging dari setiap kode porting tidak mungkin secara prinsip. Selain itu, pertanyaan tentang mengoptimalkan kode yang diterjemahkan dari perpustakaan sistem akan menjadi lebih akut daripada kode produk porting, karena panggilan ke perpustakaan sistem cenderung menjadi hambatan.

Akibatnya, diputuskan untuk mengembangkan perpustakaan sebagai seperangkat adaptor yang menyediakan akses ke fungsi yang sudah diterapkan di perpustakaan pihak ketiga, tetapi melalui API .Net-like (mirip dengan Jawa). Ini akan mengurangi pekerjaan dan menggunakan komponen C ++ yang sudah jadi, sudah dioptimalkan.

Persyaratan penting untuk kerangka kerja ini adalah bahwa kode porting harus dapat berfungsi sebagai bagian dari aplikasi pengguna (sejauh menyangkut perpustakaan). Ini berarti bahwa model manajemen memori harus dibuat jelas untuk programmer C ++, karena kita tidak dapat memaksa kode klien yang sewenang-wenang untuk berjalan di lingkungan pengumpulan sampah. Penggunaan smart pointer dipilih sebagai model kompromi. Tentang bagaimana kami berhasil memastikan transisi semacam itu (khususnya, untuk memecahkan masalah referensi melingkar), saya akan membahas dalam artikel terpisah.

Persyaratan lain adalah kemampuan untuk port tidak hanya perpustakaan, tetapi juga tes untuk mereka. Perusahaan ini memiliki budaya cakupan uji produk yang tinggi, dan kemampuan untuk menjalankan dalam C ++ tes yang sama yang ditulis untuk kode asli akan sangat menyederhanakan pencarian masalah setelah terjemahan.

Persyaratan lainnya (format peluncuran, cakupan uji, teknologi, dll.) Terutama berkaitan dengan metode bekerja dengan proyek dan pada proyek. Saya tidak akan memikirkan mereka.

Cerita


Sebelum melanjutkan, saya harus mengatakan beberapa patah kata tentang struktur perusahaan. Perusahaan bekerja dari jarak jauh, semua tim di dalamnya didistribusikan. Pengembangan produk tertentu biasanya merupakan tanggung jawab tim, disatukan oleh bahasa (hampir selalu) dan geografi (terutama).

Pekerjaan aktif pada proyek dimulai pada musim gugur 2013. Karena struktur perusahaan yang terdistribusi, dan juga karena beberapa keraguan tentang keberhasilan pengembangan, tiga versi kerangka kerja diluncurkan segera: dua di antaranya melayani satu produk masing-masing, yang ketiga mencakup tiga sekaligus. Diasumsikan bahwa ini kemudian akan menghentikan pengembangan solusi yang kurang efektif dan merealokasi sumber daya jika perlu.

Di masa depan, empat tim lagi bergabung dalam kerangka โ€œumumโ€, dua di antaranya kemudian mempertimbangkan kembali keputusan mereka dan menolak untuk merilis produk untuk C ++. Pada awal 2017, sebuah keputusan dibuat untuk menghentikan pengembangan salah satu solusi "individu" dan mentransfer tim terkait untuk bekerja dengan kerangka kerja "umum". Pengembangan yang dihentikan mengasumsikan penggunaan Boehm GC sebagai sarana manajemen memori dan berisi implementasi yang jauh lebih kaya dari beberapa bagian perpustakaan sistem, yang kemudian dipindahkan ke solusi "umum".

Dengan demikian, dua perkembangan mencapai garis akhir - yaitu, untuk merilis produk porting - satu "individu" dan satu "kolektif". Rilis pertama berdasarkan kerangka kerja kami ("umum") terjadi pada Februari 2018. Selanjutnya, rilis semua enam tim menggunakan solusi ini menjadi bulanan, dan kerangka itu sendiri dirilis sebagai produk terpisah dari perusahaan. Bahkan muncul pertanyaan tentang menjadikannya open-source, tetapi diskusi ini belum berkembang.

Tim, yang terus bekerja secara independen pada kerangka kerja yang sama, juga merilis rilis C ++ pertamanya pada 2018.

Rilis pertama berisi versi terpotong dari produk asli, yang memungkinkan untuk menunda pekerjaan penyiaran bagian yang tidak penting sebanyak mungkin. Dalam rilis berikutnya, penambahan fungsionalitas sebagian telah terjadi (dan sedang terjadi).

Organisasi kerja pada proyek


Organisasi kerja bersama pada proyek oleh beberapa tim berhasil mengalami perubahan signifikan. Pada awalnya, diputuskan bahwa satu tim besar "pusat" akan bertanggung jawab untuk mengembangkan, mendukung dan memperbaiki kerangka kerja, sementara tim "produk" kecil yang terlibat dalam pelepasan produk akhir di C ++ akan terutama bertanggung jawab untuk mencoba mengirim mereka kode dan memberikan umpan balik (informasi tentang kesalahan porting, kompilasi, dan eksekusi). Namun, skema semacam itu ternyata tidak produktif, karena tim pusat dipenuhi dengan permintaan dari semua tim "produk", dan mereka tidak dapat melanjutkan sampai masalah yang mereka temui diselesaikan.

Untuk alasan yang sebagian besar tidak tergantung pada keadaan pengembangan khusus ini, diputuskan untuk membubarkan tim "pusat" dan mentransfer orang ke tim "produk", yang sekarang bertanggung jawab untuk memperbaiki kerangka kerja sesuai kebutuhan mereka. Dalam hal ini, masing-masing tim sendiri akan membuat keputusan apakah akan menggunakan landasan bersama atau menghasilkan cabang sendiri dari proyek tersebut. Pernyataan pertanyaan semacam itu relevan untuk kerangka kerja Java, yang kodenya stabil pada waktu itu, tetapi upaya konsolidasi diperlukan untuk mengisi pustaka C ++ sesegera mungkin, sehingga tim masih bekerja sama.

Bentuk kerja ini juga memiliki kelemahan, sehingga di masa depan reformasi lain dilakukan. Tim "pusat" dipulihkan, meskipun dalam komposisi yang lebih kecil, tetapi dengan fungsi yang berbeda: sekarang tidak bertanggung jawab atas pengembangan proyek yang sebenarnya, tetapi untuk organisasi kerja bersama di dalamnya. Ini termasuk dukungan untuk lingkungan CI, mengorganisir praktik Permintaan Gabung, mengadakan pertemuan rutin dengan peserta pengembangan, mendukung dokumentasi, mencakup tes, membantu dengan solusi arsitektur dan pemecahan masalah, dan sebagainya. Selain itu, tim mengambil pekerjaan untuk menghilangkan utang teknis dan bidang-bidang padat sumber daya lainnya. Dalam mode ini, pengembangan berlanjut hingga hari ini.

Dengan demikian, proyek ini diprakarsai oleh upaya beberapa (sekitar lima) pengembang dan pada saat terbaik berjumlah sekitar dua puluh orang. Sekitar sepuluh hingga lima belas orang yang bertanggung jawab untuk pengembangan dan dukungan kerangka kerja dan pelepasan enam produk yang diangkut dapat dianggap sebagai nilai yang stabil dalam beberapa tahun terakhir.

Penulis baris ini bergabung dengan perusahaan pada pertengahan 2016, mulai bekerja di salah satu tim yang menyiarkan kode mereka menggunakan solusi "umum". Pada musim dingin tahun yang sama, ketika diputuskan untuk menciptakan kembali tim "sentral", saya pindah ke posisi pemimpin timnya. Jadi, pengalaman saya dalam proyek hari ini adalah lebih dari tiga setengah tahun.

Otonomi tim yang bertanggung jawab atas pelepasan produk-produk porting telah mengarah pada fakta bahwa dalam beberapa kasus ternyata lebih mudah bagi pengembang untuk menambah porter dengan mode operasi daripada kompromi tentang bagaimana seharusnya berperilaku secara default. Ini menjelaskan lebih dari yang Anda perkirakan, jumlah opsi yang tersedia saat mengkonfigurasi porter.

Teknologi


Saatnya berbicara tentang teknologi yang digunakan dalam proyek. Porter adalah aplikasi konsol yang ditulis dalam C #, karena dalam formulir ini lebih mudah untuk menanamkan dalam skrip yang melakukan tugas-tugas seperti "port-compile-run tests." Selain itu, ada komponen GUI yang memungkinkan Anda untuk mencapai tujuan yang sama dengan mengklik tombol.

Perpustakaan NRefactory kuno bertanggung jawab untuk parsing kode dan menyelesaikan semantik . Sayangnya, pada saat proyek dimulai, Roslyn belum tersedia, meskipun migrasi ke sana, tentu saja, ada dalam rencana kami.

Porter menggunakan trotoar kayu ASTuntuk mengumpulkan informasi dan menghasilkan kode output C ++. Ketika kode C ++ dihasilkan, representasi AST tidak dibuat, dan semua kode disimpan sebagai teks biasa.

Dalam banyak kasus, porter membutuhkan informasi tambahan untuk fine tuning. Informasi tersebut dikirimkan kepadanya dalam bentuk opsi dan atribut. Opsi berlaku untuk seluruh proyek segera dan memungkinkan Anda untuk mengatur, misalnya, nama-nama anggota makro ekspor kelas atau definisi preprocessor C # yang digunakan dalam analisis kode. Atribut digantung pada jenis dan entitas dan menentukan pemrosesan khusus untuk mereka (misalnya, kebutuhan untuk menghasilkan kata kunci "const" atau "bisa berubah" untuk anggota kelas atau untuk mengecualikan mereka dari porting).

Kelas dan struktur C # diterjemahkan ke dalam kelas C ++, anggota mereka dan kode yang dapat dieksekusi diterjemahkan ke dalam ekuivalen terdekat. Jenis dan metode umum memetakan ke templat C ++. Tautan C # diterjemahkan ke dalam pointer cerdas (kuat atau lemah) yang didefinisikan di Perpustakaan. Rincian lebih lanjut tentang prinsip-prinsip porter akan dibahas dalam artikel terpisah.

Dengan demikian, rakitan C # asli dikonversi ke proyek C ++, yang alih-alih pustaka .Net bergantung pada pustaka bersama kami. Ini ditunjukkan dalam diagram berikut:



cmake digunakan untuk membangun perpustakaan dan proyek porting. Kompiler VS 2017 dan 2019 (Windows), GCC dan Dentang (Linux) saat ini didukung.

Seperti disebutkan di atas, sebagian besar implementasi .Net kami adalah lapisan tipis dari perpustakaan pihak ketiga yang melakukan sebagian besar pekerjaan. Itu termasuk:

  • Skia - untuk bekerja dengan grafik;
  • Botan - untuk mendukung fungsi enkripsi;
  • ICU - untuk bekerja dengan string, penyandian dan budaya;
  • Libxml2 - untuk bekerja dengan XML;
  • PCRE2 - untuk bekerja dengan ekspresi reguler;
  • zlib - untuk mengimplementasikan fungsi kompresi;
  • Boost - untuk berbagai keperluan;
  • beberapa perpustakaan lain.

Baik porter dan perpustakaan dicakup dalam berbagai tes. Tes pustaka menggunakan kerangka kerja gtest. Tes porter ditulis terutama dalam NUnit / xUnit dan dibagi menjadi beberapa kategori, menyatakan bahwa:

  • output porter pada file input ini sesuai dengan target;
  • output dari program porting setelah kompilasi dan peluncuran bertepatan dengan target;
  • Tes NUnit dari proyek input berhasil dikonversi ke tes gtest di proyek porting dan lulus;
  • Ported Projects API bekerja dengan sukses di C ++;
  • dampak dari opsi dan atribut individual pada proses terjemahan adalah seperti yang diharapkan.

Kami menggunakan GitLab untuk menyimpan kode sumber . Jenkins dipilih sebagai lingkungan CI . Produk porting tersedia sebagai paket Nuget dan sebagai arsip unduhan.

Masalah


Saat mengerjakan proyek ini, kami harus menghadapi banyak masalah. Beberapa dari mereka diharapkan, sementara yang lain tampaknya sudah dalam proses. Kami mendaftar secara singkat yang utama.

  1. .Net C++.
    , C++ Object, RTTI. .Net STL.
  2. .
    , , . , C# , C++ โ€” .
  3. .
    โ€” . , . , .
  4. .
    C++ , , .
  5. C#.
    C# , C++. , :

    • , ;
    • , (, yeild);
    • , (, , , C#);
    • , C++ (, C# foreground-).
  6. .
    , .Net , .
  7. .
    - , , ยซยป , . , , , , using, -. . , .
  8. .
    , , , , , / - .
  9. .
    . , . , , , .
  10. Kesulitan dengan perlindungan kekayaan intelektual.
    Jika kode C # cukup mudah dikaburkan oleh solusi kotak, maka dalam C ++ Anda harus melakukan upaya tambahan, karena banyak anggota kelas tidak dapat dihapus dari file header tanpa konsekuensi. Menerjemahkan kelas dan metode umum ke dalam templat juga menciptakan kerentanan dengan mengekspos algoritma.

Meskipun demikian, proyek ini sangat menarik dari sudut pandang teknis. Bekerja dengannya memungkinkan Anda belajar banyak dan banyak belajar. Sifat akademis dari tugas ini juga berkontribusi terhadap hal ini.

Ringkasan


Sebagai bagian dari proyek, kami dapat menerapkan sistem yang memecahkan masalah akademik yang menarik demi aplikasi praktis langsungnya. Kami mengatur edisi bulanan perpustakaan perusahaan dalam bahasa yang awalnya tidak dimaksudkan. Ternyata sebagian besar masalah sepenuhnya dapat dipecahkan, dan solusi yang dihasilkan dapat diandalkan dan praktis.

Segera direncanakan untuk menerbitkan dua artikel lagi. Salah satunya akan menjelaskan secara rinci, dengan contoh, cara kerja porter dan bagaimana konstruksi C # ditampilkan dalam C ++. Dalam pidato lain, kita akan berbicara tentang bagaimana kita berhasil memastikan kompatibilitas model memori dua bahasa.

Saya akan mencoba menjawab pertanyaan di komentar. Jika pembaca menunjukkan minat pada aspek lain dari perkembangan kami dan jawaban mulai melampaui korespondensi dalam komentar, kami akan mempertimbangkan kemungkinan penerbitan artikel baru.

All Articles