BlackBox- генератор исходного кода (JAVA, C#, C) + Rust обработки бинарного протокола Вашего распределенного приложения


#1

Всех приветствую!

Не буду долго расписывать о своем проекте BlackBox, достаточно много уже написано.
Собственно проект развивается, уже появились и работают вложенные типы… собрался писать версию под С++ и на половину уже написал, но…
Случайно углубился в Rust и понял. Похоже версию на С++ можно уже НЕ писать. поскольку это, более Не актуально.

В процессе низкоуровневого программирования регулярно задумывался относительно проблем с которыми так успешно справляется Rust.

И поскольку одновременно активно использую другие высокоуровневые языки, собственно кодогенератор BlackBox написан на SCALA, мое вхождение в Rust пока проходит легко и непринужденно.

Уже начал первые наброски кода.
Скорее всего сначала все будет “кальками” с С / С# версий, но с Вашей помощью надеюсь это быстро устранить.

Сразу отвечу на некоторые очевидные вопросы.

Q: Это что переизобретение SerDe?
A: Нет. Это два совершенно разных идеологических подхода. В чем отличие предлагаю выяснять самостоятельно. Я готов отвечать на вопросы, но пересказывать написанное… зачем?

Q: Бинарный протокол BlackBox это какой то стандарт? Что то типа Ethernet?
A: Нет. Протокол действительно бинарный и высокооптимизированный, Но каждый раз пересоздается под конкретный проект. Метаинформация о протоколе хранится у участников обмена. Без неё разобрать протокол не реально.


#2

Честно попытался переписать, уже отлаженные и проверенные в боях, библиотеки поддержки протокола написанные на С.
Некоторые моменты мне понравились, но в целом не очень.

к примеру, выдержка из моей библиотеки на С: получение упакованных, в максимально поддерживаемый на платформе примитив, битов из байтового массива, начиная с заданного бита на заданную длину в битах

INLINER UMAX get_bits(const uint8_t* src, size_t bit, size_t bits)
{
	src += bit >> 3;
	bit &= 7;
	if (sizeof(UMAX) < bits >> 3) bits = sizeof(UMAX) << 3;
	else if (bit + bits < 9) return *src >> bit & Ol[bits];
	UMAX dst = 0;
	memcpy(&dst, src, bit + bits >> 3); 
	dst >>= bit;                      
	src += bit + bits >> 3;
	bit = bit + bits & 7; 
	if (0 < bit) dst |= (UMAX)(*src & Ol[bit]) << bits - bit;
	return dst;
}

на Rust это примерно грубо будет выглядеть так (код НЕ рабочий!!!)

fn get_bits(&self, bits: Range<usize>) -> u64 {
		let src = &self[bits.start >> 3..];
		let bit = bits.start & 7;
		let mut bits = bits.end - bits.start;
		
		if 8 < bits >> 7 { bits = 8 << 3; } else if bit + bits < 9 { src[0] >> bit & Ol[bits]; }
		
		let mut dst = 0u64;
		
		unsafe {
			use std::mem::transmute;
			let dd = transmute::<&mut u64, &mut u8>(&mut dst);
			
			use std::ptr::copy_nonoverlapping;
			copy_nonoverlapping(src.as_ptr(), dd, bit + bits >> 3);
		}
		
		dst >>= bit;        
		let src = &src[bit + bits >> 3..];
		let bit = bit + bits & 7; 
		if 0 < bit { dst |= ((src[0] & Ol[bit]) as u64) << bits - bit; }
		
		dst
	}

получается не очень… посмотрел как гуру Rust делают например в проекте NOM

#[macro_export]
macro_rules! take_bits (
  ($i:expr, $t:ty, $count:expr) => (
    {
      use $crate::lib::std::result::Result::*;
      use $crate::{Needed,IResult};

      use $crate::lib::std::ops::Div;
      use $crate::lib::std::convert::Into;
      //println!("taking {} bits from {:?}", $count, $i);
      let (input, bit_offset) = $i;
      let res : IResult<(&[u8],usize), $t> = if $count == 0 {
        Ok(( (input, bit_offset), (0 as u8).into()))
      } else {
        let cnt = ($count as usize + bit_offset).div(8);
        if input.len() * 8 < $count as usize + bit_offset {
          //println!("returning incomplete: {}", $count as usize + bit_offset);
          $crate::need_more($i, Needed::Size($count as usize))
        } else {
          let mut acc:$t            = (0 as u8).into();
          let mut offset: usize     = bit_offset;
          let mut remaining: usize  = $count;
          let mut end_offset: usize = 0;

          for byte in input.iter().take(cnt + 1) {
            if remaining == 0 {
              break;
            }
            let val: $t = if offset == 0 {
              (*byte as u8).into()
            } else {
              (((*byte as u8) << offset) as u8 >> offset).into()
            };

            if remaining < 8 - offset {
              acc += val >> (8 - offset - remaining);
              end_offset = remaining + offset;
              break;
            } else {
              acc += val << (remaining - (8 - offset));
              remaining -= 8 - offset;
              offset = 0;
            }
          }
          Ok(( (&input[cnt..], end_offset) , acc))
        }
      };
      res
    }
  );
);

сделано макросом… почему? смысл ускользает…

этот код удивил

            if remaining < 8 - offset {
              acc += val >> (8 - offset - remaining);
              end_offset = remaining + offset;
              break;
            } else {
              acc += val << (remaining - (8 - offset));
              remaining -= 8 - offset;
              offset = 0;
            }

почему не …

            if remaining < 8 - offset {
              acc += val >> (8 - offset - remaining);
              end_offset = remaining + offset;
              break;
            }

              acc += val << (remaining - (8 - offset));
              remaining -= 8 - offset;
              offset = 0;

но наибольшая засада этого кода в производительности,
на каждый вынутый из источника байт смещается на свое место, перед добавлением в итоговый аккумулятор.
в то время как разумнее сначала набить байтами аккумулятор, а потом все сместить разом, как это сделано у меня.

и это только одно место, куда я зацепился глазом.

ещё

иногда, для лучшей читабельности люблю делать вот так на С

if ((bits &= 7) == 0 || (v = bytes[last] & 0xFF & Ol[bits]) == 0) return -1;

почему бы оператору &= на Rust не возвратить итоговое значение bits , м???
да, я могу переписать под себя, но я пытаюсь понято сакральный смысл стандартной библиотеки.
может я чего то не догоняю ?

итог:

я думал будет хорошо, а вышло не очень…

отказался от мысли переписывать библиотеку поддержки протокола с С на Rust.

На Rust будет генерироваться исходный код обработки специфичный для проекта заказчика и содержащий метаинформацию протокола,
А с С библиотекой он будет взаимодействовать через код сгенеренный bindgen-ом .
Как и код на С++ , только там генерировать ничего ненужно.

из плюсов данного решения: будет одна неизменная библиотека на С которая применима как для проектов на С, так и для С++ и Rust.

любовная лодка разбилась о быт…

ну и ладно.

stay tuned!


#3

binggen завелся и исполнил свое предназначение. не сразу, но…

тем кто будет использовать:

  • если в процессе генерации обнаружатся ошибки и чтение мануала не помогает, попробуйте разные тулчейны.
    мне помогло.

  • выставляйте .layout_tests(false) иначе результат получается ужастно замусоренным.

приступаю к написанию собственно кодогенератора Rust. за основу возьму скорее всего C# версию. но вероятно это не сильно поможет, настолько Rust не похож на остальные языки.

stay tuned!


#4

В Rust - почти всё с чем пришлось столкнуться, ужасно сыро (идеально для ржавчины ? :neutral_face:).

пример bindgen

let _bindings = bindgen::Builder::default()
        .header("D:\\BlackBox\\Generator\\Code\\C\\BitsUtil.h")
        .header("D:\\BlackBox\\Generator\\Code\\C\\Host.h")
        .....

если добавление header -ов поменять местами - bindgen упадет.
если переключиться на gnu toolchain - bindgen упадет.
если в С коде попадутся совершенно невинные магические комбинации кода - bindgen упадет.

компилятор молча компилирует, и даже файл black_box.lib и все сопутствующие появляется… но компилятор Rust не может его открыть … мрак.
отложил пока.
обнаружилось кое что поважнее…

желание не переписывать С-шный код и работать с ним через FFi это конечно хорошо, но не в случае с inline функциями.
а у меня при генерации кода они активно используются. поскольку полученный пакет это обычный байтовый массив, а предложенные пользователю интерфейсы в виде удобных полей - это куча микро функций типа

fn get_bytes(src: &[u8], byte: usize, bytes: usize) -> u64 
fn set_bytes(src: u64, bytes: usize, dst: &mut [u8], byte: usize)
fn set_bits(src: u64, bits: usize, dst: &mut [u8], bit: usize)
fn get_bits(src: &[u8], bit: usize, bits: usize) -> u64

и если они на Rust будут не инлайнами - это плохо.
поэтому ту часть С функций, которая будет использована на стороне сгенеренного Rust кода придется переписать на Rust.

что и сделал.

было на С

const uint8_t         Ol[]    = {0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff};
const uint8_t         lO[]    = {0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff};
#define INLINER   inline;

INLINER UMAX get_bits(const uint8_t* src, size_t bit, size_t bits)
{
	src += bit >> 3;
	bit &= 7;
	if (sizeof(UMAX) < bits >> 3) bits = sizeof(UMAX) << 3;
	else if (bit + bits < 9) return *src >> bit & Ol[bits];
	UMAX dst = 0;
	memcpy(&dst, src, bit + bits >> 3); 
	dst >>= bit;                      
	src += bit + bits >> 3;
	bit = bit + bits & 7; 
	if (0 < bit) dst |= (UMAX)(*src & Ol[bit]) << bits - bit;
	return dst;
}

INLINER void set_bits(UMAX src, size_t bits, uint8_t* dst, size_t bit)
{
	dst += bit >> 3;
	bit &= 7;
	if (8 < bit + bits)
	{
		if (bit) 
		{
			*dst = *dst & Ol[bit] |  (src & Ol[  8 - bit]) << bit;  
			dst++;                  
			src >>= 8 - bit;      
			bits -= 8 - bit;       
		}
		memcpy(dst, &src, bits >> 3); 
		if (bits & 7)               
		{
			dst += bits >> 3;               
			*dst = *dst & lO[8 - (bits & 7)] | ((uint8_t*)&src)[(bits >> 3)]  & Ol[bits & 7]; 
		}
	}
	else * dst = *dst & (Ol[bit] | lO[8 - bit - bits]) | (src & Ol[bits]) << bit;
}


INLINER UMAX get_bytes(uint8_t* src, size_t byte, size_t bytes)
{
#ifdef   UINT64_MAX
	int32_t hi = 0;
#endif
	int32_t lo = 0;
	switch (bytes)
	{
#ifdef   UINT64_MAX
	case 8:
		hi |= (src[byte + 7] & 0xFF) << 24;
	case 7:
		hi |= (src[byte + 6] & 0xFF) << 16;
	case 6:
		hi |= (src[byte + 5] & 0xFF) << 8;
	case 5:
		hi |= src[byte + 4] & 0xFF;
#endif
	case 4:
		lo |= (src[byte + 3] & 0xFF) << 24;
	case 3:
		lo |= (src[byte + 2] & 0xFF) << 16;
	case 2:
		lo |= (src[byte + 1] & 0xFF) << 8;
	case 1:
		lo |= src[byte] & 0xFF;
	}
	return
#ifdef   UINT64_MAX
	(hi & 0xFFFFFFFFLL) << 32 |
#endif
		lo & 0xFFFFFFFFLL;
}

INLINER int32_t set_bytes(const UMAX src, const size_t bytes, uint8_t* dst, const size_t byte)
{
#ifdef   UINT64_MAX
	uint32_t hi = (uint32_t)(src >> 32);
#endif
	uint32_t lo = (uint32_t)(src & 0xFFFFFFFFL);
	switch (bytes)
	{
#ifdef   UINT64_MAX
	case 8:
		dst[byte + 7] = hi >> 24;
	case 7:
		dst[byte + 6] = hi >> 16;
	case 6:
		dst[byte + 5] = hi >> 8;
	case 5:
		dst[byte + 4] = hi & 0xFF;
#endif
	case 4:
		dst[byte + 3] = lo >> 24;
	case 3:
		dst[byte + 2] = lo >> 16;
	case 2:
		dst[byte + 1] = lo >> 8;
	case 1:
		dst[byte] = lo & 0xFF;
	}
	return byte + bytes;
}

стало на Rust

use std::mem::transmute;
use std::ptr::copy_nonoverlapping;

const Ol: [u64; 9] = [0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff];
const lO: [u64; 9] = [0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff];

#[inline(always)]
fn set_bytes(src: u64, bytes: usize, dst: &mut [u8], byte: usize) { unsafe { copy_nonoverlapping(transmute::<&u64, &u8>(&src), dst[byte..].as_mut_ptr(), bytes); } }

#[inline(always)]
fn get_bytes(src: &[u8], byte: usize, bytes: usize) -> u64 {
	let mut dst = 0u64;
	unsafe { copy_nonoverlapping(src[byte..].as_ptr(), transmute::<&mut u64, &mut u8>(&mut dst), bytes); }
	dst
}
#[inline(always)]
fn set_bits(src: u64, bits: usize, dst: &mut [u8], bit: usize) {
	let mut src = src;
	let dst = &mut dst[bit >> 3..];
	let bit = bit & 7;
	let mut bits = bits;
	
	if 8 < bit + bits
		{
			let dst = if 0 < bit 
				{
					dst[0] = (dst[0] as u64 & Ol[bit] |  (src & Ol[ 8 - bit ]) << bit) as u8;
					src >>= 8 - bit; 
					bits -= 8 - bit; 
					&mut dst[1..]
				} else { dst };
			
			set_bytes(src, bits >> 3, dst, 0);
			
			if 0 < bits & 7 
				{
					let dst = &mut dst[bits >> 3..]; 
					dst[0] = (dst[0] as u64 & lO[8 - (bits & 7)]  |  ((src >> ((bits >> 3) << 3)) & 0xFF)  &  Ol[bits & 7]) as u8; 
				}
		} else { dst[0] = (dst[0] as u64 & (Ol[bit] | lO[8 - bit - bits]) | (src as u64 & Ol[bits]) << bit) as u8; }
}

#[inline(always)]
fn get_bits(src: &[u8], bit: usize, bits: usize) -> u64 {
	let src = &src[bit >> 3..];
	let bit = bit & 7;
	let mut bits = bits;
	
	if 8 < bits >> 7 { bits = 8 << 3; } else if bit + bits < 9 { return (src[0] >> bit) as u64 & Ol[bits]; }
	
	let mut dst = get_bytes(src, 0, bit + bits >> 3);
	
	dst >>= bit;
	let src = &src[bit + bits >> 3..];
	let bit = bit + bits & 7;
	if 0 < bit { dst |= ((src[0] as u64 & Ol[bit])) << bits - bit; }
	
	dst
}

extern crate rand;

#[cfg(test)]
#[test]
fn it_works() {
	use rand::prelude::*;
	let mut rng = thread_rng();
	let mut buff = [0; 1000];
	
	for i in 0..100000
		{
			let val = rng.gen::<u64>();
			let len = rng.gen_range(1, 8);
			let pos = rng.gen_range(0, buff.len() - len);
			let expected = val & ((1u64 << (len * 8)) - 1);
			
			
			set_bytes(val, len, buff.as_mut(), pos);
			assert!(expected == get_bytes(&buff, pos, len), "val == get_bytes( &buff, {} , {} )", pos, len);
			assert!(expected == get_bits(&buff, pos * 8, len * 8), " val == get_bits( &buff, {} , {} )", pos, len);
			
			for i in buff.iter_mut() { *i = 0; }
			
			set_bits(val, len * 8, buff.as_mut(), pos * 8);
			assert!(expected == get_bytes(&buff, pos, len), "2 val == get_bytes( &buff, {} , {} )", pos, len);
			assert!(expected == get_bits(&buff, pos * 8, len * 8), "2 val == get_bits( &buff, {} , {} )", pos * 8, len * 8);
			//println!("val={} , pos={}, len={}", val, pos, len);
			
			
			let len = rng.gen_range(1, 64);
			let pos = rng.gen_range(0, buff.len() * 8 - len);
			let expected = val & ((1u64 << len) - 1);
			
			for i in buff.iter_mut() { *i = 0; }
			
			set_bits(val, len, buff.as_mut(), pos);
			assert_eq!(expected, get_bits(&buff, pos, len), " val == get_bits( &buff, {} , {} )", pos, len);
		}
}

все тесты пройдены.
предложения / замечания ?


#6

моё решение

#[repr(C)]
pub struct Meta { //in PackBytes.data content  metainformation 
    pub id: u16,
    pub _2: u32,
    pub _4: u32,
    pub _8: u32,
    pub BITS_lenINbytes_bits: u16,
    pub field_0_bit: u32,
    pub packMinBytes: u32,
    pub fields_count: u16,
}


static meta: Meta = Meta { //some embedded meta information
    id: 78, // pack id
    _2: 0,
    _4: 0,
    _8: 0,
    BITS_lenINbytes_bits: 0,
    field_0_bit: 0,
    packMinBytes: 0,// pack min length in bytes
    fields_count: 0,
};

pub struct PackBytes {
    pub meta: &'static Meta,///immutable !!!
    pub  data: [u8; 0],
}

impl PackBytes {
    #[inline]
    pub fn new(opt_bytes: isize) -> PackBytes {
        if opt_bytes < 0 { return PackBytes { meta: &meta, data: [] }; }
        
        unsafe {
            let tmp: &mut &Meta = transmute(&mut [0u8; 45]);
            *tmp = &meta;//bypass immutability
            let ret: PackBytes = transmute(tmp);
            ret
        }
    }
    
    
    #[inline]
    pub fn slice(&self) -> &mut [u8] {
        unsafe {
            let len = {
                //some code to extract len from data by means of meta
                2
            };
            
            let tmp: &mut u8 = transmute(self.data.as_ptr());
            
            ::std::slice::from_raw_parts_mut(tmp, len)
        }
    }
}


fn main()
{
    let tt = PackBytes::new(-1);
    let dd = PackBytes::new(100);
    dd.slice()[0] = 23;
    
    println!("{:?}", dd.slice()[0]);
}

пока вроде работает.

однако мои программистские шаблоны похоже треснули. объясняю:

при обмене информацией, достаточно часто возникает потребность в “пустых” пакетах. Кроме собственного id они не содержат в себе какой либо информации.
Информацией является факт их получение.

Поскольку такие пакеты неизменны, то при запросе нового подобного пакета для отправки, разумно не создавать нового пакета, а выдавать один и тот же синглтон.

в С практически нет namespace увы, увы, а в более развитых языках они есть, и например, в Java/С# такой утилитарный пакет, в одном экземпляре, разумно хранить в виде статической константы внутри описания поля.

public  abstract class PackBytes
    {
        public final Meta   meta; 
        public       byte[] data;

        public static final PackBytes pack=new PackBytes();
}

такое решение позволяло не загрязнять общее пространство имен. каждый пакет в своем namespace хранил свое барахло.

а в Rust нет статик полей(!!!). жесть.

неужели придется “захламлять” все пространство как в С???.

пока вижу выход только один. В версии под Rust под пакет будет выделятся модуль
и уже внутри namespace модуля все принадлежащее пакету.

примерно так

pub mod Pack {
  pub struct Bytes {
      pub meta: &'static Meta,///immutable !!!
      pub  data: [u8; 0],
  }

pub static pack:Bytes = Bytes { meta: &meta, data: [] };
}

так что при переписывании кода с Java / С# (возможно даже и с С++) в некоторых случаях

class == mod

а не srtuct как это может показаться


#7

class == mod
а не struct как это может показаться

Что довольно осмысленно, структура это данные, а не единица инкапсуляции.


#8

Что-то не то вы тут делаете. Добавил println!("{:?}", dd.meta.id); При каждом запуске выводятся разные значения. Похоже что dd.meta указывает куда-то в нераспределенную память.

По-моему вот тут жесть какакя-то творится:

let tmp: &mut &Meta = transmute(&mut [0u8; 45]);

Вы преобразуете ссылку на локальный массив в ссылку на ссылку на Meta.

*tmp = &meta;//bypass immutability

Здесь вы фактически в этот локальный массив загнали ссылку на статические данные &meta.

let ret: PackBytes = transmute(tmp);

А теперь все это преобразуется в PackBytes? Эта структура должна быть #[repr(C)] видимо.
В результате у вас ссылка в этой структуре( PackBytes::meta ) указывает куда-то в стек где был этот локальный массив, в который ранее загнали ссылку на статик meta.


А почему не хотите lazy_static использовать? Такое решение будет очень не безопасным. Например тесты Rust может в разных потоках запускать. Я так понял это внешний интерфейс библиотеки? А если пользователь будет PackBytes::new() из разных потоков запускать?


#10

Я странный разработчик - я очень не люблю возится с низкоуровневыми вещами и перекладывать байты из одной ссылки в другую. (это кстати не значит что я это делать не умею, я просто не люблю)

И приведенный в этом треде код просто вводит меня в почти первобытный ужас.

  1. Он небезопасный. unsafe настолько много, что возникает вопрос, зачем вообще это писать на Rust, компилятор просто не способен такой код провалидировать и никак не поможет.
  2. Он просто тяжело читается. Похоже, вы не используете rustfmt, не документируете свой код, задаете “плохие” имена переменных (“плохие” - это значит по имени переменной нельзя понять, для чего она нужна)
  3. Он выполняет странные вещи, вероятно, в попытках “оптимизировать” программу. Я не буду объяснять, почему преждевременная оптимизация без бенчмарков - плохая идея.

Я никогда не стану использовать библиотеку, код которой внушает мне опасения (и в данном случае, похоже, они не напрасны, @IBUzPE9 продемонстрировал очевидную небезопасность по памяти).

Я много раз видел, как опытные разработчики меняют язык, но не меняют свой стиль программирования. Что нормально в С (и в принципе допустимо в embedded-разработке) - плохая идея в Rust. Пока что я вижу низкоуровневый и небезопасный С код с другими ключевыми словами.


#11

let tmp: &mut &Meta = transmute(&mut [0u8; 45]);

да, тут косяк. я заметил и хотел совсем удалить это сообщение, но оно не удалилось… не судьба видать.

массивов с динамической длинной в Rusе нет. поэтому будет std::alloc

вот как правильно

impl PackBytes {
    #[inline]
    pub fn new(meta: &Meta, bytes: isize) -> PackBytes {
        unsafe {
            let layout = std::alloc::Layout::from_size_align(::std::mem::size_of::<PackBytes>() + (if bytes < 0 { 0 } else { bytes as usize }) as usize, 1).expect("Bad layout");
            
            let tmp: &mut &Meta = transmute(std::alloc::alloc_zeroed(layout));
            *tmp = meta;//bypass immutabilit
            let ret: PackBytes = transmute(tmp);
            ret
        }
    }
}

спасибо за замечания.


#12

Все почти правильно написали.

Я много раз видел

да, я только, только начал писать на Rust. о чем упомянул во первых строках…

  1. Он небезопасный. unsafe настолько много, что возникает вопрос, зачем вообще это писать на Rust, компилятор просто не способен такой код провалидировать и никак не поможет.

приведенный выше код это код буфер в black_box-sys - библиотеке обертке, которая была сгенерирована bindgen по нативными библиотеками BlackBox. Всё делаю по мануалам поэтому да, низкий уровень, не удивительно.

в комплекте будет поставлятся ещё один crate - black_box в котором уже будет Rust, только Rust и ничего кроме… и полностью сгенерированный.

Я не буду объяснять, почему преждевременная оптимизация без бенчмарков - плохая идея.

я пишу библиотеку и не могу себе позволять то, что могут позволить себе мои высокоуровневые пользователи.

Я никогда не стану использовать библиотеку, код которой внушает мне опасения (и

правильно сделаете.
поэтому помимо самого сгенерированного кода , который поставляется пользователю, еще генерируется куча тестов через которые этот код прогоняется и один из пройденых тестов также высылется +
генерируется и высылается образец правильного использования сгенерированого API


еще раз:
я в самом начале пути написания кодогенератора для RUST, задача осложняется тем что я только - только начал понимать язык.


Iterator да или нет
#13

пройдена первая веха.
проект в первый раз успешно сгенерировался и скомпилировался,

это не значит, что он полностью готов.
это значит, что с архитектурой в целом наступила полная ясность. теперь на очереди кодогенерация Тестов и примера использования сгенерированного кода.

И,
затем,
отладка.