JavaScript, kendi kendine temizlik yapmayı affeder ancak statik diller kesinlikle kolay değildir...
Squoosh.app, ne kadar farklı resim codec'lerinin kullanıldığını gösteren bir PWA'dır ve ayarlar, kaliteyi önemli ölçüde etkilemeden resim dosyasının boyutunu iyileştirebilir. Ancak, C++ veya Rust ile yazılmış kitaplıkları nasıl alıp erişebileceğinizi gösteren teknik bir demo yardımcı olur.
Mevcut ekosistemlerden kod taşımak son derece değerlidir ancak bununla birlikte, JavaScript ile JavaScript arasındaki farkları azaltır. Bunlardan biri farklı yaklaşımlarını anlatacağım.
JavaScript, kendi kendine temizlik yapmayı affedebilecek olsa da bu tür statik diller kesinlikle hayır. Açıkça yeni bir ayrılmış anı istemeniz gerekir ve bunu yaparken geri vermeniz ve bir daha kullanmamanız gerekir. Aksi halde sızıntılar yaşanır... oldukça düzenli bir şekilde gerçekleşir. Bu bellek sızıntılarını nasıl ayıklayabileceğinize ve hatalardan kaçınmak için kodunuzu nasıl tasarlayacağınızı
Şüpheli desen
Son zamanlarda, Squoosh üzerinde çalışmaya başlarken, Google C++ codec sarmalayıcıları. Şimdi ImageQuant sarmalayıcısına örnek (yalnızca nesne oluşturma ve dağıtım kısımlarını gösterecek şekilde küçültülmüş):
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
// …
free(image8bit);
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
}
void free_result() {
free(result);
}
JavaScript (TypeScript):
export async function process(data: ImageData, opts: QuantizeOptions) {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = module.quantize(/* … */);
module.free_result();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
Bir sorun fark ettiniz mi? İpucu: ücretsiz sonrası kullanım, JavaScript!
Emscripten'de typed_memory_view
, WebAssembly (Wasm) tarafından desteklenen bir JavaScript Uint8Array
döndürür.
byteOffset
ve byteLength
, belirtilen işaretçi ve uzunluğa ayarlanmış şekilde bellek arabelleği. Ana
noktası, bunun bir WebAssembly bellek arabelleğine değil, bir TypedArray görünümü
Verilerin JavaScript'e ait kopyası.
JavaScript'ten free_result
işlevini çağırdığımızda, aynı zamanda tablodaki bir standart C işlevini free
sonraki ayırmalar için kullanılabilir olarak ayarlanmış olacaktır. Bu, Uint8Array
için rastgele veriler, ileride Wasm'e yapılacak bir çağrıyla rastgele verilerin üzerine yazılabilir.
Veya bir free
uygulanması, boşta kalan belleğin hemen sıfır olarak doldurulmasına bile karar verebilir. İlgili içeriği oluşturmak için kullanılan
Emscripten'in kullandığı free
bunu yapmaz ancak burada bir uygulama ayrıntısından yararlanıyoruz
garanti edilemez.
İşaretçinin arkasındaki bellek korunsa bile, yeni ayırmanın
WebAssembly belleği. WebAssembly.Memory
, JavaScript API aracılığıyla veya buna karşılık gelen bir şekilde büyütüldüğünde
memory.grow
talimatı, mevcut ArrayBuffer
öğesini ve geçişli olarak tüm görünümleri geçersiz kılar
yardımcı oluyor.
Bu davranışı göstermek için Geliştirici Araçları (veya Node.js) konsolunu kullanmama izin verin:
> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}
> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42
> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
// (the size of the buffer is 1 WebAssembly "page" == 64KB)
> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data
> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!
> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one
Son olarak, free_result
ile new
Uint8ClampedArray
arasında tekrar Wasm'ı açıkça aramasak bile bir noktada codec'lerimize çoklu iş parçacığı desteği ekleyebiliriz. Bu durumda,
biz klonlamayı başamadan hemen önce verilerin üzerine yazan tamamen farklı bir ileti dizisi olabilir.
Bellek hataları aranıyor
Her ihtimale karşı, bu kodda pratikte herhangi bir sorun olup olmadığını kontrol etmeye karar verdim. Bu yeni, Emscripten dezenfektanlarını denemek için mükemmel bir fırsat gibi görünüyor. desteği ve Chrome Geliştirici Zirvesi'ndeki WebAssembly konuşmasında sunuldu:
Bu örnekte,
AddressSanitizer
gibi çeşitli işaretçi ve bellekle ilgili sorunları algılayabilir. Bunu kullanmak için codec'imizi yeniden derlememiz gerekir.
-fsanitize=address
ile:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
node_modules/libimagequant/libimagequant.a
Bu işlem, işaretçi güvenlik kontrollerini otomatik olarak etkinleştirir ancak potansiyel hafızayı da bulmak istiyoruz. sızdır. ImageQuant'ı program yerine kitaplık olarak kullandığımız için "çıkış noktası" yok. ile Emscripten, tüm belleğin boşaltıldığını otomatik olarak doğrulayabilir.
Bunun yerine, bu gibi durumlarda LeakSanitizer (AddressSanitizer'da yer alır) içine
__lsan_do_leak_check
ve
__lsan_do_recoverable_leak_check
,
Bu komut, tüm belleğin serbest bırakılmasını beklediğimiz ve bunu doğrulamak istediğimizde manuel olarak
varsayımı. __lsan_do_leak_check
, çalışan bir uygulamanın sonunda kullanılmak üzere tasarlanmıştır.
bir sızıntı tespit edilirse işlemi iptal etmek isteyebilir, __lsan_do_recoverable_leak_check
gibi kütüphane kullanım alanları, sızıntıları konsola yazdırmak istediğinizde daha uygundur.
uygulamanın çalışmaya devam etmesini sağlar.
İkinci yardımcıyı Embind aracılığıyla gösterelim. Böylece istediğimiz zaman JavaScript'ten çağırabiliriz:
#include <sanitizer/lsan_interface.h>
// …
void free_result() {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result);
function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}
Ve resimle işiniz bittiğinde, bunu JavaScript tarafından çağırın. Bu işlem, C++ yerine JavaScript tarafı, tüm kapsamların doğru şekilde çıktı ve tüm geçici C++ nesneleri serbest bırakıldı.
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
Bu işlem, konsolda aşağıdakine benzer bir rapor sunar:
Maalesef bazı küçük eksiklikler var ancak yığın izleme, bu fonksiyonların tüm adlarında saplanmışlar. Bu bilgileri korumak için temel hata ayıklama bilgileriyle yeniden derleyelim:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
-g2 \
node_modules/libimagequant/libimagequant.a
Bu, çok daha iyi görünüyor:
Yığın izlemenin bazı bölümleri Emscripten dahili öğelerini işaret ettiği için hâlâ belirsiz görünüyor ancak
sızıntıların RawImage
dönüşümünden "kablo türü"ne geldiğini söyle (bir JavaScript değerine)
Embind. Gerçekten de koda baktığımızda, RawImage
C++ örneğini döndürdüğümüzü
ama hiçbir zaman iki tarafta da serbest bırakmıyoruz.
Hatırlatalım, şu anda JavaScript ile Google arasında çöp toplama entegrasyonu
WebAssembly geliştirilmekte olan olsa da. Bunun yerine
manuel olarak boşaltmak ve
nesnesini tanımlayın. Özellikle Embind için resmi
dokümanlar
açığa çıkan C++ sınıflarında bir .delete()
yöntemi çağrılmasını önerin:
JavaScript kodunun, aldığı tüm C++ nesne tanıtıcılarını veya sonsuza kadar büyüyecektir.
var x = new Module.MyClass; x.method(); x.delete();
Aslında bunu sınıfımız için JavaScript'te yaptığımızda:
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
Sızıntı beklendiği gibi kaybolur.
Dezenfektanlarla ilgili diğer sorunları keşfetme
Dezenfektan içeren diğer Squoosh codec'lerini derlemek hem benzer hem de bazı yeni sorunları ortaya çıkarır. Örneğin, örneğin, MozJPEG bağlamalarında şu hatayı alıyorum:
Burada bu bir sızıntı değil, ayrılmış sınırların dışındaki bir anıya yazıyoruz 😅
MozJPEG kodunu incelediğimizde, buradaki sorunun jpeg_mem_dest
olduğunu tespit ettik.
JPEG için bir bellek hedefi ayırmak üzere kullandığımız işlev—JPEG'nin mevcut değerlerini
outbuffer
ve outsize
aynı
sıfır olmayan bir sayı:
if (*outbuffer == NULL || *outsize == 0) {
/* Allocate initial buffer */
dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
if (dest->newbuffer == NULL)
ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
*outsize = OUTPUT_BUF_SIZE;
}
Ancak, bu değişkenlerden hiçbirini başlatmadan çağırırız. Bu, MozJPEG tarafından bu değişkenlerde depolanan, rastgele bir bellek adresine dönüşür zamanı geldi.
uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);
Çağrı bu sorunu çözmeden önce her iki değişkeni sıfır başlatma işlemi ve artık kod bellek sızıntısı kontrolü yapın. Neyse ki kontrol başarıyla geçti. Bu da şu anda olabilir.
Paylaşılan durumla ilgili sorunlar
...yoksa biz mi?
codec bağlamalarımızın durumun bir kısmını depoladığını ve bu işlem sırasında global statik değişkenlerini ifade eder ve MozJPEG özellikle karmaşık yapılara sahiptir.
uint8_t* last_result;
struct jpeg_compress_struct cinfo;
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
// …
}
Bunlardan bazıları ilk çalıştırmada gecikmeli olarak başlatılır ve daha sonra ileride düzgün bir şekilde tekrar kullanılırsa ne olur? çalışır mı? Böylece dezenfektanla tek bir arama yapıldığında bu müşteri sorunlu olarak bildirilmez.
Şimdi, farklı kalite seviyelerinde rastgele tıklayarak resmi birkaç kez işlemeyi deneyelim görüntülenir. Gerçekten de artık aşağıdaki raporu elde ediyoruz:
262.144 bayt. Örnek resmin tamamı jpeg_finish_compress
kaynağından sızdırılmış gibi görünüyor.
Dokümanları ve resmi örnekleri incelediğimizde jpeg_finish_compress
önceki jpeg_mem_dest
çağrımız tarafından ayrılan hafızada yer açmaz. Bu işlem yalnızca
bu sıkıştırma yapısı, belleğimizi zaten bilse bile
hedef... Ah.
Bunu, free_result
işlevindeki verileri manuel olarak serbest bırakarak düzeltebiliriz:
void free_result() {
/* This is an important step since it will release a good deal of memory. */
free(last_result);
jpeg_destroy_compress(&cinfo);
}
Bu hafıza hatalarını tek tek araştırmaya devam edebilirim ama şimdiye kadar hafıza yönetimine mevcut bir yaklaşımla birlikte bazı kötü sistematik sorunlar ortaya çıkabilir.
Bazıları dezenfektana hemen yakalanabilir. Bazıları ise karmaşık numaraların yakalanabilmesini gerektirir. Son olarak, gönderinin başındaki günlüklerden gördüğümüz gibi dezenfektana hiç yakalanmıyor. Bunun nedeni, asıl hatalı kullanımın Dezenfektanın görünürlüğü olmadığı JavaScript tarafı. Bu sorunlar, projenin .
Güvenli bir sarmalayıcı oluşturma
Birkaç adım geri gidip kodu yeniden yapılandırarak tüm bu sorunları giderelim. sağlayabilir. Yine örnek olarak ImageQuant sarmalayıcısını kullanacağım, ancak benzer yeniden düzenleme kuralları geçerlidir yanı sıra diğer benzer kod tabanları için de kolayca yükleyebilirsiniz.
Öncelikle, ücretsiz sonrası kullanım sorununu yayının başlangıcından itibaren düzeltelim. Bunun için kullanarak verileri JavaScript tarafında serbest olarak işaretlemeden önce WebAssembly destekli görünümden klonlayın:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
return imgData;
}
Şimdi, çağrılar arasında genel değişkenlerde herhangi bir durum paylaşmayacağımızdan emin olalım. Bu hem de önceden gördüğümüz sorunların bazılarını giderecek hem de iş parçacıklı bir ortamda codec'lerini destekler.
Bunu yapmak için C++ sarmalayıcısını yeniden düzenleyerek işleve yapılan her çağrının kendi çağrısını yönettiğinden emin oluruz.
verileri nasıl kullanacağınızı göstereceğim. Sonra, free_result
fonksiyonumuzun imzasını
geri işaretçiyi kabul edin:
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_attr* attr = liq_attr_create();
liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_result* res = nullptr;
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
uint8_t* result = (uint8_t*)malloc(size * 4);
// …
}
void free_result() {
void free_result(uint8_t *result) {
free(result);
}
Ancak, JavaScript ile etkileşim kurmak için zaten Emscripten'de Embind kullandığımızdan C++ bellek yönetimi ayrıntılarını tamamen gizleyerek API'yi daha da güvenli hale getirin!
Bunun için, new Uint8ClampedArray(…)
kısmını JavaScript'ten C++ tarafına,
Embind. Sonra, geri dönmeden önce bile verileri JavaScript belleğine klonlamak için bunu kullanabiliriz
işlevini kullanın:
class RawImage {
public:
val buffer;
int width;
int height;
RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
RawImage quantize(/* … */) {
val quantize(/* … */) {
// …
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
val js_result = Uint8ClampedArray.new_(typed_memory_view(
image_width * image_height * 4,
result
));
free(result);
return js_result;
}
Tek bir değişiklikle her ikimizin de sonuçtaki bayt dizisinin JavaScript'e ait olmasını nasıl sağlayacağımızı unutmayın
ve WebAssembly belleği tarafından desteklenmez ve önceden sızdırılan RawImage
sarmalayıcıdan kurtulabilirsiniz.
çok önemlidir.
JavaScript'in artık verileri serbest bırakma konusunda endişe duymasına gerek kalmaz ve sonuçları çöp toplanan diğer nesneler:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
// module.doLeakCheck();
return imgData;
return new ImageData(result, result.width, result.height);
}
Bu aynı zamanda, C++ tarafında artık özel bir free_result
bağlamasına ihtiyaç duymadığımız anlamına da gelir:
void free_result(uint8_t* result) {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
class_<RawImage>("RawImage")
.property("buffer", &RawImage::buffer)
.property("width", &RawImage::width)
.property("height", &RawImage::height);
function("quantize", &quantize);
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result, allow_raw_pointers());
}
Sonuç olarak, sarmalayıcı kodumuz aynı zamanda daha temiz ve daha güvenli hale geldi.
Bunun ardından, ImageQuant sarmalayıcı kodunda ve diğer codec'ler için çoğaltılmış benzer bellek yönetimi düzeltmeleri. Daha fazla bilgi almak isterseniz oluşturulan PR'yi burada görebilirsiniz: C++ için bellek düzeltmeleri codec'ler ile uyumludur.
Çıkarımlar
Bu yeniden düzenlemeden diğer kod tabanlarına uygulanabilecek hangi dersleri çıkarıp paylaşabiliriz?
- WebAssembly tarafından desteklenen bellek görünümlerini, kullandığı dilden bağımsız olarak olduğunu unutmayın. Bundan daha uzun süre hayatta kalacaklarına güvenemezsiniz. geleneksel yöntemlerle yakalamaktır. Dolayısıyla, verileri daha sonra kullanmak üzere saklamanız gerekiyorsa kopyalayıp orada depolamaya çalışın.
- Mümkünse doğrudan ham işaretçilerle çalışmayı sağlar. Bu işlem, sizi JavaScript Override WebAssembly üzerindeki hatalardan kurtarmaz ancak en azından statik dil kodu tarafından yetiştirilen hataların yüzeyini azaltır.
- Hangi dili kullanıyor olursanız olun, geliştirme sırasında temizleyicilerle kod çalıştırın. Temizleyiciler,
Sadece statik dil kodundaki sorunları değil, aynı zamanda JavaScript kodu ile ilgili bazı sorunları da yakalayabilirsiniz.
WebAssembly sınırı;
.delete()
çağrısı yapmayı unutma veya şuradan geçersiz işaretçileri iletme: çok önemli. - Mümkünse yönetilmeyen verileri ve nesneleri WebAssembly'den JavaScript'e göstermekten kaçının. JavaScript, çöp toplanan bir dildir ve manuel bellek yönetimi yaygın değildir. Bu durum, WebAssembly dilinizin bellek modelinin soyutlama sızıntısı olarak değerlendirilebilir ve yanlış yönetim JavaScript kod tabanında kolayca gözden kaçabilir.
- Bu, açıkça görülse de diğer tüm kod tabanlarında olduğu gibi, değişken durumu global biçimde depolamaktan kaçının. değişkenlerine karşılık gelir. Ya da çeşitli çağrılarda yeniden kullanımıyla ilgili sorunlarda hata ayıklamak veya ileti dizileri olduğundan, olabildiğince bağımsız tutmak en iyisidir.