Как не переписать проект на Rust

Original author: Майкл Брайан (Michael F. Bryan)
  • Translation
  • Tutorial

Как только вы переступаете через болевой порог Борроу-Чекера и осознаёте, что Rust позволяет вытворять невообразимые (и порой опасные) в других языках вещи, вас может постигнуть настолько же непреодолимое желание Переписать Всё на Rust. Хоть и в лучшем случае это банально непродуктивно (бессмысленное разбазаривание усилий на несколько проектов), а в худшем — приводит к уменьшению качества кода (ведь с чего вы считаете себя более опытным в области применения библиотеки, чем её изначальный автор?)


Гораздо полезнее будет предоставить безопасный интерфейс для оригинальной библиотеки, повторно используя её код.


Первые шаги
Собираем chmlib-sys
Пишем безопасную обёртку на Rust
  • Поиск элементов по имени
  • Обход элементов по фильтру
  • Чтение содержимого файлов
Добавляем примеры
  • Оглавление CHM-файла
  • Распаковка CHM-файла на диск
Что дальше?


В этой статье рассматривается реальный проект. Мне надо было вытащить информацию из существующих CHM-файлов, а времени разбираться в формате не было. Лень — двигатель прогресса.

Крейт chmlib опубликован на crates.io, его исходный код доступен на GitHub. Если вы нашли его полезным или нашли в нём проблемы, то дайте мне знать через багтрекер.

Первые шаги


Для начала стоит разобраться в том, как изначально задумывалась работа с библиотекой.


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

Не пропускайте этот шаг!

Мы будем работать с CHMLib, библиотекой на Си для чтения файлов Microsoft Compiled HTML Help (.chm).


Начнём с создания нового проекта и подключения CHMLib в виде git-подмодуля:


$ git init chmlib && cd chmlib
  Initialized empty Git repository in /home/michael/Documents/chmlib/.git/
$ touch README.md Cargo.toml
$ cargo new --lib chmlib
  Created library `chmlib` package
$ cargo new --lib chmlib-sys
  Created library `chmlib-sys` package
$ cat Cargo.toml
  [workspace]
  members = ["chmlib", "chmlib-sys"]
$ git submodule add git@github.com:jedwing/CHMLib.git vendor/CHMLib
  Cloning into '/home/michael/Documents/chmlib/vendor/CHMLib'...
  remote: Enumerating objects: 99, done.
  remote: Total 99 (delta 0), reused 0 (delta 0), pack-reused 99
  Receiving objects: 100% (99/99), 375.51 KiB | 430.00 KiB/s, done.
  Resolving deltas: 100% (45/45), done.

После этого глянем, что там внутри, с помощью tree:


$ tree vendor/CHMLib
vendor/CHMLib
├── acinclude.m4
├── AUTHORS
├── ChangeLog
├── ChmLib-ce.zip
├── ChmLib-ds6.zip
├── configure.in
├── contrib
│   └── mozilla_helper.sh
├── COPYING
├── Makefile.am
├── NEWS
├── NOTES
├── README
└── src
    ├── chm_http.c
    ├── chm_lib.c
    ├── chm_lib.h
    ├── enum_chmLib.c
    ├── enumdir_chmLib.c
    ├── extract_chmLib.c
    ├── lzx.c
    ├── lzx.h
    ├── Makefile.am
    ├── Makefile.simple
    └── test_chmLib.c

2 directories, 23 files

Похоже, библиотека использует GNU Autotools для сборки. Это нехорошо, потому что всем пользователям крейта chmlib (и их пользователям) потребуется устанавливать Autotools.


Мы постараемся избавиться от этой «заразной» зависимости, собирая код на Си вручную, но об этом позже.

Файлы lzx.h и lzx.c содержат реализацию алгоритма сжатия LZX. Вообще лучше было бы использовать какую-нибудь библиотеку liblzx, чтобы получать обновления бесплатно и всё такое, но, пожалуй, проще будет тупо скомпилировать эти файлы.


enum_chmLib.c, enumdir_chmLib.c, extract_chmLib.c, похоже, являются примерами использования функций chm_enumerate(), chm_enumerate_dir(), chm_retrieve_object(). Это пригодится...


В файле test_chmLib.c находится ещё один пример, на это раз извлекающий одну страницу из CHM-файла на диск.


chm_http.c реализует простой HTTP-сервер, показывающий CHM-файл в браузере. Вот это, наверное, уже не пригодится.


Вот мы и разобрали всё, что находится в vendor/CHMLib/src. Будем собирать библиотеку?


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


$ clang chm_lib.c enum_chmLib.c -o enum_chmLib
  /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `chm_close':
  chm_lib.c:(.text+0x8fa): undefined reference to `LZXteardown'
  /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_region':
  chm_lib.c:(.text+0x18ca): undefined reference to `LZXinit'
  /usr/bin/ld: /tmp/chm_lib-537dfe.o: in function `_chm_decompress_block':
  chm_lib.c:(.text+0x2900): undefined reference to `LZXreset'
  /usr/bin/ld: chm_lib.c:(.text+0x2a4b): undefined reference to `LZXdecompress'
  /usr/bin/ld: chm_lib.c:(.text+0x2abe): undefined reference to `LZXreset'
  /usr/bin/ld: chm_lib.c:(.text+0x2bf4): undefined reference to `LZXdecompress'
  clang: error: linker command failed with exit code 1 (use -v to see invocation)

Ладненько, может этот LZX всё же нужен...


$ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib

Э-э-э… и всё?


Чтобы убедиться в работоспособности кода, я скачал пример из Интернета:


$ curl http://www.innovasys.com/static/hs/samples/topics.classic.chm.zip \
           -o topics.classic.chm.zip
$ unzip topics.classic.chm.zip
Archive:  topics.classic.chm.zip
  inflating: output/compiled/topics.classic.chm
$ file output/compiled/topics.classic.chm
output/compiled/topics.classic.chm: MS Windows HtmlHelp Data

Посмотрим, как с ним справится enum_chmLib:


$ ./enum_chmLib output/compiled/topics.classic.chm
output/compiled/topics.classic.chm:
 spc    start   length   type           name
 ===    =====   ======   ====           ====
   0        0        0   normal dir     /
   1  5125797     4096   special file       /#IDXHDR
   ...
   1  4944434    11234   normal file        /BrowserView.html
   ...
   0        0        0   normal dir     /flash/
   1   532689      727   normal file        /flash/expressinstall.swf
   0        0        0   normal dir     /Images/Commands/RealWorld/
   1    24363     1254   normal file        /Images/Commands/RealWorld/BrowserBack.bmp
   ...
   1    35672     1021   normal file        /Images/Employees24.gif
   ...
   1  3630715   200143   normal file        /template/packages/jquery-mobile/script/
                                             jquery.mobile-1.4.5.min.js
   ...
   0      134     1296   meta file      ::DataSpace/Storage/MSCompressed/Transform/
                                          {7FC28940-9D31-11D0-9B27-00A0C91E9C7C}/
                                          InstanceData/ResetTable

Господи, даже здесь jQuery ¯\_(ツ)_/¯


Собираем chmlib-sys


Теперь мы знаем достаточно, чтобы использовать CHMLib в крейте chmlib-sys, который отвечает за сборку нативной библиотеки, линковку её компилятором Раста, и интерфейс к функциям на Си.


Для сборки библиотеки нужно написать файл build.rs. С помощью крейта cc он вызовёт компилятор Си и сделает прочую дружбомагию, чтобы всё работало вместе как надо.


Нам повезло, что мы можем переложить большую часть работы на cc, но порой бывает значительно труднее. Подробнее читайте в документации на сборочные скрипты.

Сперва добавим cc как зависимость для chmlib-sys:


$ cd chmlib-sys
$ cargo add --build cc
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding cc v1.0.46 to build-dependencies

Затем напишем build.rs:


// chmlib-sys/build.rs

use cc::Build;
use std::{env, path::PathBuf};

fn main() {
    let project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
        .canonicalize()
        .unwrap();
    let root_dir = project_dir.parent().unwrap();
    let src = root_dir.join("vendor").join("CHMLib").join("src");

    Build::new()
        .file(src.join("chm_lib.c"))
        .file(src.join("lzx.c"))
        .include(&src)
        .warnings(false)
        .compile("chmlib");
}

Ещё надо рассказать Cargo о том, что chmlib-sys линкуется с библиотекой chmlib. Тогда Cargo сможет гарантировать, что во всём графе зависимостей присутствует только один крейт, зависящий от конкретной нативной библиотеки. Это позволяет избежать непонятных сообщений об ошибках про повторяющиеся символы или случайного использования несовместимых библиотек.


--- a/chmlib-sys/Cargo.toml
+++ b/chmlib-sys/Cargo.toml
@@ -3,7 +3,13 @@ name = "chmlib-sys"
 version = "0.1.0"
 authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
 edition = "2018"
 description = "Raw bindings to the CHMLib C library"
 license = "LGPL"
 repository = "https://github.com/Michael-F-Bryan/chmlib"
+links = "chmlib"
+build = "build.rs"

 [dependencies]

 [build-dependencies]
 cc = { version = "1.0" }

Дальше нам остаётся объявить все функции, экспортируемые библиотекой chmlib, чтобы их можно было использовать из Раста.


Именно для этого и существует замечательный проект bindgen. На вход ему отдаётся заголовочный файл на Си, а на выходе получается файл с FFI-привязками для Раста.


$ cargo install bindgen
$ bindgen ../vendor/CHMLib/src/chm_lib.h \
    -o src/lib.rs \
    --raw-line '#![allow(non_snake_case, non_camel_case_types)]'
$ head src/lib.rs
  /* automatically generated by rust-bindgen */

  #![allow(non_snake_case, non_camel_case_types)]

  pub const CHM_UNCOMPRESSED: u32 = 0;
  pub const CHM_COMPRESSED: u32 = 1;
  pub const CHM_MAX_PATHLEN: u32 = 512;
  pub const CHM_PARAM_MAX_BLOCKS_CACHED: u32 = 0;
  pub const CHM_RESOLVE_SUCCESS: u32 = 0;
  pub const CHM_RESOLVE_FAILURE: u32 = 1;
$ tail src/lib.rs
  extern "C" {
      pub fn chm_enumerate_dir(
          h: *mut chmFile,
          prefix: *const ::std::os::raw::c_char,
          what: ::std::os::raw::c_int,
          e: CHM_ENUMERATOR,
          context: *mut ::std::os::raw::c_void,
      ) -> ::std::os::raw::c_int;
  }

Очень рекомендую почитать руководство пользователя Bindgen, если вам надо что-то подправить в его выхлопе.

На этом этапе полезно будет написать smoke-тест, который проверит, что всё работает как положено и мы действительно можем вызывать функции оригинальной библиотеки на Си.


// chmlib-sys/tests/smoke_test.rs

// Нам потребуется преобразовать Path в char* с нулевым байтом в конце.
// К сожалению, OsStr (и Path) на Windows используют [u16] под капотом,
// поэтому их нельзя так просто преобразовать в char*.
#![cfg(unix)]

use std::{ffi::CString, os::unix::ffi::OsStrExt, path::Path};

#[test]
fn open_example_file() {
    let project_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let sample_chm = project_dir.parent().unwrap().join("topics.classic.chm");
    let c_str = CString::new(sample_chm.as_os_str().as_bytes()).unwrap();

    unsafe {
        let handle = chmlib_sys::chm_open(c_str.as_ptr());
        assert!(!handle.is_null());
        chmlib_sys::chm_close(handle);
    }
}

cargo test говорит, что вроде всё в порядке:


$ cargo test
    Finished test [unoptimized + debuginfo] target(s) in 0.03s
     Running ~/chmlib/target/debug/deps/chmlib_sys-2ffd7b11a9fd8437

running 1 test
test bindgen_test_layout_chmUnitInfo ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running ~/chmlib/target/debug/deps/smoke_test-f7be9810412559dc

running 1 test
test open_example_file ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests chmlib-sys

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Пишем безопасную обёртку на Rust


Техни-и-ически мы теперь можем вызывать CHMLib из Раста, но это требует кучи unsafe. Для наколенной поделки может и сойдёт, но для публикации на crates.io стоит написать безопасную обёртку для всего небезопасного кода.


Если посмотреть на API chmlib-sys с помощью cargo doc --open, то в нём видно много функций, которые принимают *mut ChmFile как первый аргумент. Это похоже на объекты и методы.


Заголовочный файл CHMLib
/* $Id: chm_lib.h,v 1.10 2002/10/09 01:16:33 jedwin Exp $ */
/***************************************************************************
 *             chm_lib.h - CHM archive manipulation routines               *
 *                           -------------------                           *
 *                                                                         *
 *  author:     Jed Wing <jedwin@ugcs.caltech.edu>                         *
 *  version:    0.3                                                        *
 *  notes:      These routines are meant for the manipulation of microsoft *
 *              .chm (compiled html help) files, but may likely be used    *
 *              for the manipulation of any ITSS archive, if ever ITSS     *
 *              archives are used for any other purpose.                   *
 *                                                                         *
 *              Note also that the section names are statically handled.   *
 *              To be entirely correct, the section names should be read   *
 *              from the section names meta-file, and then the various     *
 *              content sections and the "transforms" to apply to the data *
 *              they contain should be inferred from the section name and  *
 *              the meta-files referenced using that name; however, all of *
 *              the files I've been able to get my hands on appear to have *
 *              only two sections: Uncompressed and MSCompressed.          *
 *              Additionally, the ITSS.DLL file included with Windows does *
 *              not appear to handle any different transforms than the     *
 *              simple LZX-transform.  Furthermore, the list of transforms *
 *              to apply is broken, in that only half the required space   *
 *              is allocated for the list.  (It appears as though the      *
 *              space is allocated for ASCII strings, but the strings are  *
 *              written as unicode.  As a result, only the first half of   *
 *              the string appears.)  So this is probably not too big of   *
 *              a deal, at least until CHM v4 (MS .lit files), which also  *
 *              incorporate encryption, of some description.               *
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Lesser General Public License as        *
 *   published by the Free Software Foundation; either version 2.1 of the  *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 ***************************************************************************/

#ifndef INCLUDED_CHMLIB_H
#define INCLUDED_CHMLIB_H

#ifdef __cplusplus
extern "C" {
#endif

/* RWE 6/12/1002 */
#ifdef PPC_BSTR
#include <wtypes.h>
#endif

#ifdef WIN32
#ifdef __MINGW32__
#define __int64 long long
#endif
typedef unsigned __int64 LONGUINT64;
typedef __int64          LONGINT64;
#else
typedef unsigned long long LONGUINT64;
typedef long long          LONGINT64;
#endif

/* the two available spaces in a CHM file                      */
/* N.B.: The format supports arbitrarily many spaces, but only */
/*       two appear to be used at present.                     */
#define CHM_UNCOMPRESSED (0)
#define CHM_COMPRESSED   (1)

/* structure representing an ITS (CHM) file stream             */
struct chmFile;

/* structure representing an element from an ITS file stream   */
#define CHM_MAX_PATHLEN  (512)
struct chmUnitInfo
{
    LONGUINT64         start;
    LONGUINT64         length;
    int                space;
    int                flags;
    char               path[CHM_MAX_PATHLEN+1];
};

/* open an ITS archive */
#ifdef PPC_BSTR
/* RWE 6/12/2003 */
struct chmFile* chm_open(BSTR filename);
#else
struct chmFile* chm_open(const char *filename);
#endif

/* close an ITS archive */
void chm_close(struct chmFile *h);

/* methods for ssetting tuning parameters for particular file */
#define CHM_PARAM_MAX_BLOCKS_CACHED 0
void chm_set_param(struct chmFile *h,
                   int paramType,
                   int paramVal);

/* resolve a particular object from the archive */
#define CHM_RESOLVE_SUCCESS (0)
#define CHM_RESOLVE_FAILURE (1)
int chm_resolve_object(struct chmFile *h,
                       const char *objPath,
                       struct chmUnitInfo *ui);

/* retrieve part of an object from the archive */
LONGINT64 chm_retrieve_object(struct chmFile *h,
                              struct chmUnitInfo *ui,
                              unsigned char *buf,
                              LONGUINT64 addr,
                              LONGINT64 len);

/* enumerate the objects in the .chm archive */
typedef int (*CHM_ENUMERATOR)(struct chmFile *h,
                              struct chmUnitInfo *ui,
                              void *context);
#define CHM_ENUMERATE_NORMAL    (1)
#define CHM_ENUMERATE_META      (2)
#define CHM_ENUMERATE_SPECIAL   (4)
#define CHM_ENUMERATE_FILES     (8)
#define CHM_ENUMERATE_DIRS      (16)
#define CHM_ENUMERATE_ALL       (31)
#define CHM_ENUMERATOR_FAILURE  (0)
#define CHM_ENUMERATOR_CONTINUE (1)
#define CHM_ENUMERATOR_SUCCESS  (2)
int chm_enumerate(struct chmFile *h,
                  int what,
                  CHM_ENUMERATOR e,
                  void *context);

int chm_enumerate_dir(struct chmFile *h,
                      const char *prefix,
                      int what,
                      CHM_ENUMERATOR e,
                      void *context);

#ifdef __cplusplus
}
#endif

#endif /* INCLUDED_CHMLIB_H */

Начнём с типа данных, который в конструкторе вызывает chm_open(), а в деструкторе — chm_close().


pub unsafe extern "C" fn chm_open(filename: *const c_char) -> *mut chmFile;
pub unsafe extern "C" fn chm_close(h: *mut chmFile);

Для упрощения обработки ошибок мы используем крейт thiserror, которые автоматически реализует std::error::Error.


$ cd chmlib
$ cargo add thiserror

Теперь надо придумать, как превратить std::path::Path в *const c_char. К сожалению, это не так-то просто сделать из-за разных приколов с совместимостью.


// chmlib/src/lib.rs

use thiserror::Error;
use std::{ffi::CString, path::Path};

#[cfg(unix)]
fn path_to_cstring(path: &Path) -> Result<CString, InvalidPath> {
    use std::os::unix::ffi::OsStrExt;
    let bytes = path.as_os_str().as_bytes();
    CString::new(bytes).map_err(|_| InvalidPath)
}

#[cfg(not(unix))]
fn path_to_cstring(path: &Path) -> Result<CString, InvalidPath> {
    // К сожалению, на Windows CHMLib использует CreateFileA(), поэтому она
    // умеет работать только с путями в ASCII. Я не знаю... давайте просто
    // верить, что не будет использовать Юникод и тут ничего не сломается?
    let rust_str = path.as_os_str().as_str().ok_or(InvalidPath)?;
    CString::new(rust_str).map_err(|_| InvalidPath)
}

#[derive(Error, Debug, Copy, Clone, PartialEq)]
#[error("Invalid Path")]
pub struct InvalidPath;

Теперь определим структуру ChmFile. Она хранит ненулевой указатель на chmlib_sys::chmFile. Если chm_open() возвращает нулевой указатель, то значит у неё не получилось открыть файл из-за какой-то ошибки.


// chmlib/src/lib.rs

use std::{ffi::CString, path::Path, ptr::NonNull};

#[derive(Debug)]
pub struct ChmFile {
    raw: NonNull<chmlib_sys::chmFile>,
}

impl ChmFile {
    pub fn open<P: AsRef<Path>>(path: P) -> Result<ChmFile, OpenError> {
        let c_path = path_to_cstring(path.as_ref())?;

        // безопасно, потому что c_path корректный
        unsafe {
            let raw = chmlib_sys::chm_open(c_path.as_ptr());

            match NonNull::new(raw) {
                Some(raw) => Ok(ChmFile { raw }),
                None => Err(OpenError::Other),
            }
        }
    }
}

impl Drop for ChmFile {
    fn drop(&mut self) {
        unsafe {
            chmlib_sys::chm_close(self.raw.as_ptr());
        }
    }
}

/// The error returned when we are unable to open a [`ChmFile`].
#[derive(Error, Debug, Copy, Clone, PartialEq)]
pub enum OpenError {
    #[error("Invalid path")]
    InvalidPath(#[from] InvalidPath),
    #[error("Unable to open the ChmFile")]
    Other,
}

Чтобы убедиться в отсутствии утечек памяти, запустим простой тест под Valgrind. Он создаст ChmFile и сразу же его освободит.


// chmlib/src/lib.rs

#[test]
fn open_valid_chm_file() {
    let sample = sample_path();

    // открыть файл
    let chm_file = ChmFile::open(&sample).unwrap();
    // и тут же его закрыть
    drop(chm_file);
}

fn sample_path() -> PathBuf {
    let project_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let sample = project_dir.parent().unwrap().join("topics.classic.chm");
    assert!(sample.exists());

    sample
}

Valgrind говорит, что неучтённой памяти не осталось:


$ valgrind ../target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file
==8953== Memcheck, a memory error detector
==8953== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8953== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==8953== Command: ~/chmlib/target/debug/deps/chmlib-8d8c740d578324 open_valid_chm_file
==8953==

running 1 test
test tests::open_valid_chm_file ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

==8953==
==8953== HEAP SUMMARY:
==8953==     in use at exit: 0 bytes in 0 blocks
==8953==   total heap usage: 249 allocs, 249 frees, 43,273 bytes allocated
==8953==
==8953== All heap blocks were freed -- no leaks are possible
==8953==
==8953== For counts of detected and suppressed errors, rerun with: -v
==8953== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Поиск элементов по имени


На очереди функция chm_resolve_object():


pub const CHM_RESOLVE_SUCCESS: u32 = 0;
pub const CHM_RESOLVE_FAILURE: u32 = 1;
/* resolve a particular object from the archive */
pub unsafe extern "C" fn chm_resolve_object(
    h: *mut chmFile,
    objPath: *const c_char,
    ui: *mut chmUnitInfo
) -> c_int;

Поиск может завершиться ошибкой, поэтому chm_resolve_object() возвращает код ошибки, сообщающий об успехе или неудаче, а информация о найденном объекте будет записана по переданному указателю на chmUnitInfo.


Тип std::mem::MaybeUninit создан как раз для нашего случая с out-параметром ui.


Пока что оставим структуру UnitInfo пустой — это Rust-эквивалент C-структуры chmUnitInfo. Поля мы добавим, когда начнём что-то читать из ChmFile.


// chmlib/src/lib.rs

impl ChmFile {
    ...

    /// Find a particular object in the archive.
    pub fn find<P: AsRef<Path>>(&mut self, path: P) -> Option<UnitInfo> {
        let path = path_to_cstring(path.as_ref()).ok()?;

        unsafe {
            // создаём неинициализированную chmUnitInfo на стеке
            let mut resolved = MaybeUninit::<chmlib_sys::chmUnitInfo>::uninit();

            // попробуем что-нибудь найти
            let ret = chmlib_sys::chm_resolve_object(
                self.raw.as_ptr(),
                path.as_ptr(),
                resolved.as_mut_ptr(),
            );

            if ret == chmlib_sys::CHM_RESOLVE_SUCCESS {
                // в случае успеха "resolved" будет инициализированной
                Some(UnitInfo::from_raw(resolved.assume_init()))
            } else {
                None
            }
        }
    }
}

#[derive(Debug)]
pub struct UnitInfo;

impl UnitInfo {
    fn from_raw(ui: chmlib_sys::chmUnitInfo) -> UnitInfo { UnitInfo }
}

Заметьте, что ChmFile::find() принимает &mut self, хотя код на Расте не содержит явного изменения состояния. Дело в том, что реализация на Си использует всякие fseek() для перемещения по файлу, поэтому внутреннее состояние всё же изменяется при поиске.

Проверим ChmFile::find() на подопытном файле, который мы ранее скачали:


// chmlib/src/lib.rs

#[test]
fn find_an_item_in_the_sample() {
    let sample = sample_path();
    let chm = ChmFile::open(&sample).unwrap();

    assert!(chm.find("/BrowserView.html").is_some());
    assert!(chm.find("doesn't exist.txt").is_none());
}

Обход элементов по фильтру


CHMLib предоставляет API для просмотра содержимого CHM-файла через фильтр по битовой маске.


Возьмём удобный крейт bitflags для работы с масками и флажками:


$ cargo add bitflags
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding bitflags v1.2.1 to dependencies

И определим флажки Filter на основе констант из chm_lib.h:


// chmlib/src/lib.rs

bitflags::bitflags! {
    pub struct Filter: c_int {
        /// A normal file.
        const NORMAL = chmlib_sys::CHM_ENUMERATE_NORMAL as c_int;
        /// A meta file (typically used by the CHM system).
        const META = chmlib_sys::CHM_ENUMERATE_META as c_int;
        /// A special file (starts with `#` or `$`).
        const SPECIAL = chmlib_sys::CHM_ENUMERATE_SPECIAL as c_int;
        /// It's a file.
        const FILES = chmlib_sys::CHM_ENUMERATE_FILES as c_int;
        /// It's a directory.
        const DIRS = chmlib_sys::CHM_ENUMERATE_DIRS as c_int;
    }
}

Ещё нам понадобится extern "C"-адаптер для Растовых замыканий, который можно передать в Си в виде указателя на функцию:


// chmlib/src/lib.rs

unsafe extern "C" fn function_wrapper<F>(
    file: *mut chmlib_sys::chmFile,
    unit: *mut chmlib_sys::chmUnitInfo,
    state: *mut c_void,
) -> c_int
where
    F: FnMut(&mut ChmFile, UnitInfo) -> Continuation,
{
    // предотвращаем утечки паник за границы FFI-вызова
    let result = panic::catch_unwind(|| {
        // Мы используем ManuallyDrop чтобы передать ссылку `&mut ChmFile`
        // но при этом не хотим вызывать деструктор (избегая double-free).
        let mut file = ManuallyDrop::new(ChmFile {
            raw: NonNull::new_unchecked(file),
        });
        let unit = UnitInfo::from_raw(unit.read());
        // указатель state гарантировано указывает на подходящее замыкание
        let closure = &mut *(state as *mut F);
        closure(&mut file, unit)
    });

    match result {
        Ok(Continuation::Continue) => {
            chmlib_sys::CHM_ENUMERATOR_CONTINUE as c_int
        },
        Ok(Continuation::Stop) => chmlib_sys::CHM_ENUMERATOR_SUCCESS as c_int,
        Err(_) => chmlib_sys::CHM_ENUMERATOR_FAILURE as c_int,
    }
}

function_wrapper содержит хитрый unsafe-код, которым надо уметь пользоваться:

  • Указатель state должен указывать на экземпляр замыкания F.
  • Код на Расте, исполняемый замыканием, может вызывать панику. Она не должна пересекать границу между Растом и Си, так как раскрутка стека на разных языках — это неопределённое поведение. Возможную панику следует перехватить с помощью std::panic::catch_unwind().
  • Указатель на chmlib_sys::chmFile, передаваемый в function_wrapper, также хранится в вызывающем ChmFile. На время вызова надо гарантировать, что только замыкание может манипулировать chmlib_sys::chmFile, иначе может возникнуть состояние гонки.
  • Замыканию надо передать &mut ChmFile, а для этого потребуется создать временный объект на стеке, используя имеющийся указатель. Однако, если при этом отработает деструктор ChmFile, то chmlib_sys::chmFile будет освобождён слишком рано. Для решения этой проблемы существует std::mem::ManuallyDrop.

Вот как function_wrapper используется для реализации ChmFile::for_each():


// chmlib/src/lib.rs

impl ChmFile {
    ...

    /// Inspect each item within the [`ChmFile`].
    pub fn for_each<F>(&mut self, filter: Filter, mut cb: F)
    where
        F: FnMut(&mut ChmFile, UnitInfo) -> Continuation,
    {
        unsafe {
            chmlib_sys::chm_enumerate(
                self.raw.as_ptr(),
                filter.bits(),
                Some(function_wrapper::<F>),
                &mut cb as *mut _ as *mut c_void,
            );
        }
    }

    /// Inspect each item within the [`ChmFile`] inside a specified directory.
    pub fn for_each_item_in_dir<F, P>(
        &mut self,
        filter: Filter,
        prefix: P,
        mut cb: F,
    ) where
        P: AsRef<Path>,
        F: FnMut(&mut ChmFile, UnitInfo) -> Continuation,
    {
        let path = match path_to_cstring(prefix.as_ref()) {
            Ok(p) => p,
            Err(_) => return,
        };

        unsafe {
            chmlib_sys::chm_enumerate_dir(
                self.raw.as_ptr(),
                path.as_ptr(),
                filter.bits(),
                Some(function_wrapper::<F>),
                &mut cb as *mut _ as *mut c_void,
            );
        }
    }
}

Обратите внимание на то, как параметр F взаимодействует с обобщённой функцией function_wrapper. Такой приём часто применяется, когда надо передать замыкание Rust через FFI в код на другом языке.

Чтение содержимого файлов


Последняя функция, которая нам нужна, отвечает за собственно чтение файла с помощью chm_retrieve_object().


Её реализация довольно тривиальная. Это похоже на типичный трейт std::io::Read, за исключением явного смещения в файле.


// chmlib/src/lib.rs

impl ChmFile {
    ...

    pub fn read(
        &mut self,
        unit: &UnitInfo,
        offset: u64,
        buffer: &mut [u8],
    ) -> Result<usize, ReadError> {
        let mut unit = unit.0.clone();

        let bytes_written = unsafe {
            chmlib_sys::chm_retrieve_object(
                self.raw.as_ptr(),
                &mut unit,
                buffer.as_mut_ptr(),
                offset,
                buffer.len() as _,
            )
        };

        if bytes_written >= 0 {
            Ok(bytes_written as usize)
        } else {
            Err(ReadError)
        }
    }
}

#[derive(Error, Debug, Copy, Clone, PartialEq)]
#[error("The read failed")]
pub struct ReadError;

Конечно, было бы неплохо иметь более детализированное сообщение об ошибке, чем «не вышло прочитать», но судя по исходному коду, chm_retrieve_object() не особо различает ошибки:


  • возвращает 0, когда файл дочитан до конца;
  • возвращает 0 при неправильных аргументах: нулевых указателях или выходе за границы;
  • возвращает −1 при ошибках чтения файлах системой (и заполняет errno);
  • возвращает −1 при ошибках распаковки, не различая порчу данных и, скажем, невозможность выделить память под временный буфер через malloc().

Протестировать ChmFile::read() можно с помощью файлов с известным содержимым:


// chmlib/src/lib.rs

#[test]
fn read_an_item() {
    let sample = sample_path();
    let mut chm = ChmFile::open(&sample).unwrap();
    let filename = "/template/packages/core-web/css/index.responsive.css";

    // этот файл должен быть в тестовом архиве
    let item = chm.find(filename).unwrap();

    // считаем его во временный буфер
    let mut buffer = vec![0; item.length() as usize];
    let bytes_written = chm.read(&item, 0, &mut buffer).unwrap();

    // он должен быть полностью заполнен
    assert_eq!(bytes_written, item.length() as usize);

    // ...и содержать то, что мы ожидаем
    let got = String::from_utf8(buffer).unwrap();
    assert!(got.starts_with(
        "html, body, div#i-index-container, div#i-index-body"
    ));
}

Добавляем примеры


Мы покрыли большую часть API библиотеки CHMLib и многие на этом бы закончили работу, считая портирование успешно завершённым. Однако, было бы неплохо сделать наш крейт ещё более удобным для пользователей. Этой цели служат примеры кода и документация — я заметил, что в сообществе Rust и Go этим моментам уделяется довольно много внимания (наверное потому, что rustdoc и godoc хорошо интегрированы в языковую среду).


К счастью, в CHMLib уже есть примеры кода, так что нам будет достаточно просто портировать их.


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


Оглавление CHM-файла


Этот пример открывает CHM-файл и печатает таблицу с информаций обо всех его элементах.


Оригинальный пример на Си
/* $Id: enum_chmLib.c,v 1.7 2002/10/09 12:38:12 jedwin Exp $ */
/***************************************************************************
 *          enum_chmLib.c - CHM archive test driver                        *
 *                           -------------------                           *
 *                                                                         *
 *  author:     Jed Wing <jedwin@ugcs.caltech.edu>                         *
 *  notes:      This is a quick-and-dirty test driver for the chm lib      *
 *              routines.  The program takes as its input the paths to one *
 *              or more .chm files.  It attempts to open each .chm file in *
 *              turn, and display a listing of all of the files in the     *
 *              archive.                                                   *
 *                                                                         *
 *              It is not included as a particularly useful program, but   *
 *              rather as a sort of "simplest possible" example of how to  *
 *              use the enumerate portion of the API.                      *
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Lesser General Public License as        *
 *   published by the Free Software Foundation; either version 2.1 of the  *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 ***************************************************************************/

#include "chm_lib.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*
 * callback function for enumerate API
 */
int _print_ui(struct chmFile *h,
              struct chmUnitInfo *ui,
              void *context)
{
    static char szBuf[128];
    memset(szBuf, 0, 128);
    if(ui->flags & CHM_ENUMERATE_NORMAL)
        strcpy(szBuf, "normal ");
    else if(ui->flags & CHM_ENUMERATE_SPECIAL)
        strcpy(szBuf, "special ");
    else if(ui->flags & CHM_ENUMERATE_META)
        strcpy(szBuf, "meta ");

    if(ui->flags & CHM_ENUMERATE_DIRS)
        strcat(szBuf, "dir");
    else if(ui->flags & CHM_ENUMERATE_FILES)
        strcat(szBuf, "file");

    printf("   %1d %8d %8d   %s\t\t%s\n",
           (int)ui->space,
           (int)ui->start,
           (int)ui->length,
           szBuf,
           ui->path);
    return CHM_ENUMERATOR_CONTINUE;
}

int main(int c, char **v)
{
    struct chmFile *h;
    int i;

    for (i=1; i<c; i++)
    {
        h = chm_open(v[i]);
        if (h == NULL)
        {
            fprintf(stderr, "failed to open %s\n", v[i]);
            exit(1);
        }

        printf("%s:\n", v[i]);
        printf(" spc    start   length   type\t\t\tname\n");
        printf(" ===    =====   ======   ====\t\t\t====\n");

        if (! chm_enumerate(h,
                            CHM_ENUMERATE_ALL,
                            _print_ui,
                            NULL))
            printf("   *** ERROR ***\n");

        chm_close(h);
    }

    return 0;
}

Функцию _print_ui() легко переписать на Rust. Она всего лишь генерирует текстовое описание из флажков UnitInfo и строк, а потом немного шаманит с выравниванием, чтобы таблица выглядела как таблица.


// chmlib/examples/enumerate-items.rs

fn describe_item(item: UnitInfo) {
    let mut description = String::new();

    if item.is_normal() {
        description.push_str("normal ");
    } else if item.is_special() {
        description.push_str("special ");
    } else if item.is_meta() {
        description.push_str("meta ");
    }

    if item.is_dir() {
        description.push_str("dir");
    } else if item.is_file() {
        description.push_str("file");
    }

    println!(
        "   {} {:8} {:8}   {}\t\t{}",
        item.space(),
        item.start(),
        item.length(),
        description,
        item.path().unwrap_or(Path::new("")).display()
    );
}

Функция main() на скорую руку разбирает аргументы командной строки, открывает файл, и вызывает describe_item() через ChmFile::for_each().


// chmlib/examples/enumerate-items.rs

fn main() {
    let filename = env::args()
        .nth(1)
        .unwrap_or_else(|| panic!("Usage: enumerate-items <filename>"));

    let mut file = ChmFile::open(&filename).expect("Unable to open the file");

    println!("{}:", filename);
    println!(" spc    start   length   type\t\t\tname");
    println!(" ===    =====   ======   ====\t\t\t====");

    file.for_each(Filter::all(), |_file, item| {
        describe_item(item);
        Continuation::Continue
    });
}

Для проверки давайте сравним вывод примера на Расте с оригиналом:


$ cargo run --example enumerate-items topics.classic.chm > rust-example.txt
$ cd vendor/CHMLib/src
$ clang chm_lib.c enum_chmLib.c lzx.c -o enum_chmLib
$ cd ../../..
$ ./vendor/CHMLib/src/enum_chmLib topics.classic.chm > c-example.txt
$ diff -u rust-example.txt c-example.txt
$ echo $?
0

diff говорит, что всё совпадает, но давайте убедимся, что он бы увидел разницу, если бы она была. Добавим в вывод какой-нибудь мусор и посмотрим, что на это скажет diff.


diff --git a/chmlib/examples/enumerate-items.rs b/chmlib/examples/enumerate-items.rs
index e68fa58..ef855ac 100644
--- a/chmlib/examples/enumerate-items.rs
+++ b/chmlib/examples/enumerate-items.rs
@@ -36,6 +36,10 @@ fn describe_item(item: UnitInfo) {
         description.push_str("file");
     }

+    if item.length() % 7 == 0 {
+        description.push_str(" :)");
+    }
+
     println!(
         "   {} {:8} {:8}   {}\t\t{}",
         item.space(),

Запускаем тест с новым кодом:


$ cargo run --example enumerate-items topics.classic.chm > rust-example.txt
$ diff -u rust-example.txt c-example.txt
--- rust-example.txt    2019-10-20 16:51:53.933560892 +0800
+++ c-example.txt       2019-10-20 16:40:42.007053966 +0800
@@ -1,9 +1,9 @@
 topics.classic.chm:
  spc    start   length   type            name
  ===    =====   ======   ====            ====
-   0        0        0   normal dir :)       /
+   0        0        0   normal dir          /
    1  5125797     4096   special file        /#IDXHDR
-   0        0        0   special file :)     /#ITBITS
+   0        0        0   special file        /#ITBITS
    1  5104520      148   special file        /#IVB
    1  5132009     1227   special file        /#STRINGS
    0     1430     4283   special file        /#SYSTEM
@@ -13,9 +13,9 @@
...

Победа!


Распаковка CHM-файла на диск


Другой пример, идущий в комплекте с CHMLib, извлекает все «обычные» файлы на диск.


Оригинальный пример на Си
/* $Id: extract_chmLib.c,v 1.4 2002/10/10 03:24:51 jedwin Exp $ */
/***************************************************************************
 *          extract_chmLib.c - CHM archive extractor                       *
 *                           -------------------                           *
 *                                                                         *
 *  author:     Jed Wing <jedwin@ugcs.caltech.edu>                         *
 *  notes:      This is a quick-and-dirty chm archive extractor.           *
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Lesser General Public License as        *
 *   published by the Free Software Foundation; either version 2.1 of the  *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 ***************************************************************************/

#include "chm_lib.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
#include <windows.h>
#include <direct.h>
#define mkdir(X, Y) _mkdir(X)
#define snprintf _snprintf
#else
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#endif

struct extract_context
{
    const char *base_path;
};

static int dir_exists(const char *path)
{
#ifdef WIN32
        /* why doesn't this work?!? */
        HANDLE hFile;

        hFile = CreateFileA(path,
                        FILE_LIST_DIRECTORY,
                        0,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL);
        if (hFile != INVALID_HANDLE_VALUE)
        {
        CloseHandle(hFile);
        return 1;
        }
        else
        return 0;
#else
        struct stat statbuf;
        if (stat(path, &statbuf) != -1)
                return 1;
        else
                return 0;
#endif
}

static int rmkdir(char *path)
{
    /*
     * strip off trailing components unless we can stat the directory, or we
     * have run out of components
     */

    char *i = strrchr(path, '/');

    if(path[0] == '\0'  ||  dir_exists(path))
        return 0;

    if (i != NULL)
    {
        *i = '\0';
        rmkdir(path);
        *i = '/';
        mkdir(path, 0777);
    }

#ifdef WIN32
        return 0;
#else
    if (dir_exists(path))
        return 0;
    else
        return -1;
#endif
}

/*
 * callback function for enumerate API
 */
int _extract_callback(struct chmFile *h,
              struct chmUnitInfo *ui,
              void *context)
{
    LONGUINT64 ui_path_len;
    char buffer[32768];
    struct extract_context *ctx = (struct extract_context *)context;
    char *i;

    if (ui->path[0] != '/')
        return CHM_ENUMERATOR_CONTINUE;

    /* quick hack for security hole mentioned by Sven Tantau */
    if (strstr(ui->path, "/../") != NULL)
    {
        /* fprintf(stderr, "Not extracting %s (dangerous path)\n", ui->path); */
        return CHM_ENUMERATOR_CONTINUE;
    }

    if (snprintf(buffer, sizeof(buffer), "%s%s", ctx->base_path, ui->path) > 1024)
        return CHM_ENUMERATOR_FAILURE;

    /* Get the length of the path */
    ui_path_len = strlen(ui->path)-1;

    /* Distinguish between files and dirs */
    if (ui->path[ui_path_len] != '/' )
    {
        FILE *fout;
        LONGINT64 len, remain=ui->length;
        LONGUINT64 offset = 0;

        printf("--> %s\n", ui->path);
        if ((fout = fopen(buffer, "wb")) == NULL)
    {
        /* make sure that it isn't just a missing directory before we abort */
        char newbuf[32768];
        strcpy(newbuf, buffer);
        i = strrchr(newbuf, '/');
        *i = '\0';
        rmkdir(newbuf);
        if ((fout = fopen(buffer, "wb")) == NULL)
              return CHM_ENUMERATOR_FAILURE;
    }

        while (remain != 0)
        {
            len = chm_retrieve_object(h, ui, (unsigned char *)buffer, offset, 32768);
            if (len > 0)
            {
                fwrite(buffer, 1, (size_t)len, fout);
                offset += len;
                remain -= len;
            }
            else
            {
                fprintf(stderr, "incomplete file: %s\n", ui->path);
                break;
            }
        }

        fclose(fout);
    }
    else
    {
        if (rmkdir(buffer) == -1)
            return CHM_ENUMERATOR_FAILURE;
    }

    return CHM_ENUMERATOR_CONTINUE;
}

int main(int c, char **v)
{
    struct chmFile *h;
    struct extract_context ec;

    if (c < 3)
    {
        fprintf(stderr, "usage: %s <chmfile> <outdir>\n", v[0]);
        exit(1);
    }

    h = chm_open(v[1]);
    if (h == NULL)
    {
        fprintf(stderr, "failed to open %s\n", v[1]);
        exit(1);
    }

    printf("%s:\n", v[1]);
    ec.base_path = v[2];
    if (! chm_enumerate(h,
                        CHM_ENUMERATE_ALL,
                        _extract_callback,
                        (void *)&ec))
        printf("   *** ERROR ***\n");

    chm_close(h);

    return 0;
}

Реализация на Си довольно многословна ввиду отсутствия в языке высокоуровневых абстракций и слабой стандартной библиотеки. Надеюсь, наш пример будет более читабельным.


Интерес здесь представляет функция extract(). Код в принципе самоочевиден, так что я не буду особо утруждать себя его пересказом своими словами на русском.


// chmlib/examples/extract.rs

fn extract(
    root_dir: &Path,
    file: &mut ChmFile,
    item: &UnitInfo,
) -> Result<(), Box<dyn Error>> {
    if !item.is_file() || !item.is_normal() {
        // меня интересуют только обычные файлы
        return Ok(());
    }
    let path = match item.path() {
        Some(p) => p,
        // нет пути так нет пути, едем дальше
        None => return Ok(()),
    };

    let mut dest = root_dir.to_path_buf();
    // Примечание: в CHM все пути к обычным файлам абсолютные (начинаются с "/"),
    // поэтому при конкатенации с root_dir надо удалить ведущий символ "/".
    dest.extend(path.components().skip(1));

    // гарантируем существование родительской директории
    if let Some(parent) = dest.parent() {
        fs::create_dir_all(parent)?;
    }

    let mut f = File::create(dest)?;
    let mut start_offset = 0;
    // CHMLib не даёт прямого доступа к &[u8] с содержимым файла (например,
    // потому что файл может быть сжатым), так что нам следует собирать его
    // по кусочкам во временном буфере
    let mut buffer = vec![0; 1 << 16];

    loop {
        let bytes_read = file.read(item, start_offset, &mut buffer)?;
        if bytes_read == 0 {
            // дошли до конца файла
            break;
        } else {
            // записываем этот фрагмент и продолжаем
            start_offset += bytes_read as u64;
            f.write_all(&buffer)?;
        }
    }

    Ok(())
}

Функция main() существенно проще, чем extract(), и от предыдущего примера отличается только обработкой ошибок распаковки.


// chmlib/examples/extract.rs

fn main() {
    let args: Vec<_> = env::args().skip(1).collect();
    if args.len() != 2 || args.iter().any(|arg| arg.contains("-h")) {
        println!("Usage: extract <chm-file> <out-dir>");
        return;
    }

    let mut file = ChmFile::open(&args[0]).expect("Unable to open the file");

    let out_dir = PathBuf::from(&args[1]);

    file.for_each(Filter::all(), |file, item| {
        match extract(&out_dir, file, &item) {
            Ok(_) => Continuation::Continue,
            Err(e) => {
                eprintln!("Error: {}", e);
                Continuation::Stop
            },
        }
    });
}

Натравив собранный пример на подопытный CHM-файл мы получаем пачку HTML-файлов, которые можно открыть в обычном веб-браузере.


$ cargo run --example extract -- ./topics.classic.chm ./extracted
$ tree ./extracted
./extracted
├── default.html
├── BrowserForward.html
...
├── Images
│   ├── Commands
│   │   └── RealWorld
│   │       ├── BrowserBack.bmp
...
├── script
│   ├── _community
│   │   └── disqus.js
│   ├── hs-common.js
...
└── userinterface.html
$ firefox topics.classic/default.html
(открывает default.html в Firefox)

Часть JavaScript не работает (подозреваю какие-то особенности родного браузера Microsoft Help), поиска тоже нет, но в целом жить можно.


Что дальше?


Крейт chmlib теперь полностью функционален и, за исключением пары мелочей, готов к публикации на crates.io.


Несколько вопросов остаются читателю в качестве домашнего задания:


  • Если замыкание в ChmFile::for_each() или ChmFile::for_each_item_in_dir() запаникует, то после возврата из Си в Раст следует продолжить панику, а не просто вернуть ошибку.
  • Было бы неплохо, чтобы для типичного случая прохода по всем файлам в ChmFile нам не надо было явно возвращать Continuation::Continue во всех замыканиях. Вероятно, тут стоило бы принимать F: FnMut(&mut ChmFile, UnitInfo) -> C где C: Into<Continuation>, после чего реализовать impl From<()> for Continuation.
  • Ошибки во время обхода файла (например, как у нас в extract()) хорошо бы возвращать назад из ChmFile::for_each() и прерывать обход. Это можно совместить с предыдущим пунктом с помощью impl<E> From<Result<(), E>> for Continuation where E: Error + 'static.
  • Как-то не очень красиво вручную копировать кусочки файла во временный буфер перед записью их в std::fs::File. Удобнее было бы добавить оптимизированную функцию, которая в цикле вызывает ChmFile::read() и перекладывает результат в какой-нибудь std::io::Writer.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 88

    +3
    Гораздо полезнее будет предоставить безопасный интерфейс для оригинальной библиотеки, повторно используя её код.

    Ну ну
    www.reddit.com/r/rust/comments/biq864/giving_up_on_wlrootsrs
      +4
      Тут еще стоит вспомнить как Tox сервер переписывали на раст и нашли ошибку в C версии.
        +2

        Как говорится, it depends. Если у библиотеки упоротый API, то ей может помочь новый API. Для этого вовсе не обязательно переписывать именно на Раст — можно просто переписать, чтобы избавиться от исторических наслоений (конечно же добавив новых багов, но не в этом суть), перебрать код по косточкам, и так далее. Это приятно с точки зрения программирования, но с точки зрения инженерии в общем случае энергию лучше было бы направить написание и тестирование приложений, использующих библиотеку, чтобы найти практические проблемы. Вот если у этих практических проблем огромная история и все просто жрут кактус, тогда есть смысл задуматься о переписывании.


        Правда, open source часто держится на идеалистах, которым по барабану вся эта инженерия и экономическая целесообразность, и потому «из ниоткуда» появляются библиотеки и проекты, без которых люди потом жить не могут. Особенно в случае Раста, где количество компетентных желающих на нём писать существенно больше, чем количество желающих за это платить. Творческую энергию надо куда-то направлять. Вместе со стабилизацией async/await это желание только возрастёт.

          +5
          можно просто переписать, чтобы избавиться от исторических наслоений
          Так не лучше ли переписать сразу на безопасном Rust чем продолжать страдать с C?

          но с точки зрения инженерии в общем случае энергию лучше было бы направить написание и тестирование приложений
          По мне так проще системно перейти на то что тебя страхует чем жрать кактус. Это я про новые проекты или полные переписывания. Понятно что огромную кодовую базу C/C++ крайне сложно/мало реально/практично переписывать на что то еще.

          Правда, open source часто держится на идеалистах
          Тут можно привести альтернативную точку зрения что бизнес думает локально, только здесь и сейчас потому что завтра он закроется. В будущее он крайне редко смотрит. А здесь и сейчас это корыто как то работает.

          Особенно в случае Раста, где количество компетентных желающих на нём писать существенно больше, чем количество желающих за это платить
          В этом в том числе и апологеты C/C++ не помогают кстати.
            +2
            >Понятно что огромную кодовую базу C/C++ крайне сложно/мало реально/практично переписывать на что то еще.
            Понятно что в общем случае это наверное реально так — но в данном конкретном вряд ли. Ну посудите сами, что такого может быть в формате .chm, чтобы его нельзя было переписать на любой вменяемый язык за осмысленное время? Я пожалуй знаю только несколько примеров, когда такое не проходит. Один из них — когда некоторые алгоритмы (шифрование, условно) просто не открыты.

            Если нет низкоуровневого мухлежа с железом, жесткой экономии памяти, подсчета тактов и т.п. — переписывание обычно проходит довольно легко. В конкретно данном случае я бы даже не задумывался (при условии что потребность реальна).
              +1
              Тут дело в другом, если переписывать библиотеку какого то ПО на Rust, значит что придется следить за всеми изменениями и вносить правки в свою версию. В случае биндингов — только за актуальностью биндингов. Кроме того в большом софте реально много кода, это просто непрактично переписывать.
                +1
                Не читал статью. Просто положил в закладки, а трекер упорно показывает активность в комментах. В принципе пока мне близка Ваша позиция.

                От себя могу добавить только одно — довольно молодой язык. С ещё не устоявшимся API. Боюсь, что до тех пор пока выход каждой новой версии может сможет сильно изменять двоичный результат не о каком широком практическом применении речи быть не может. Это, кстати, отчасти и является причиной того, что даже C++ не приживается во встраиваемых системах. Уж больно часто его дергают. А вот чистый C, как «кросплатформенный ассемблер» давно устоялся. И если и идут работы вокруг него, так только в части исправления откровенных косяков компиляции и оптимизации runtime библиотеки (опять же, которую во встраиваемых системах стараются использовать самым минимальным образом).

                Впрочем, надо посмотреть. Только время все расставит по своим местам. Пока Rust любопытен. Пожалуй, я готов его изучать, но точно не готов писать не нем то, что уйдет в массовое производство. Пока есть сомнения. И в том, что это кто-то кроме меня сможет сопровождать, и в надежности результата в том числе. Да, последнее скорее всего напрасно, но пока оно есть — не о каком программировании для изделия речи не будет. Только «на побаловаться». Ну и скорость… Куда ж без нее. Такты наше все. Чем меньше их съем я, тем больше их останется тем кто выше.
                  0
                  Не очень понятно зачем стабильное ABI для железок. Зерокостная сериализация/десериализация и без ABI делается, .so/.dll там вроде как не юзаются, а для чего еще — непонятно. Хотя вы видимо говорите про перфоманс получающегося кода, если он часто меняется то может незаметно просесть, верно?
                  Апишка в std устоялась, а вот в сам язык просто добавляют полезные фичи. Можно и без них обойтись. Еще по части асинхронных библиотек все очень бурно идет на данный момент (из-за только что стабилизировавшегося async/await), но пользоваться уже можно (люди еще в 2016-2017 на нестабильном в проде сидели вполне успешно). В принципе работа по части embeded вполне себе ведется сообществом, и есть no_std крейты под некоторые микроконтроллеры. Сам не занимаюсь поэтому точно сказать не могу.

                  В отличие от C/C++ тут можно локализировать unsafe и работать на безопасных обертках, что снижает количество потенциальных ошибок. Для меня например C и особенно C++ страшны именно количеством UB на каждом шагу.

                  По поводу тактов и прочего — тут также как и с C надо просто бенчмаркать, профилировать и следить за тем какой код получается, особых отличий наверное нету?
                    0
                    Хотя вы видимо говорите про перфоманс получающегося кода, если он часто меняется то может незаметно просесть, верно?


                    Так точно. Завтра найдется хитрая и крайне редко возможная (и не возможная принципиально у меня) возможность переполнения, которую исправят ценой производительности и поломают все мои критичные ко времени выполнения участки. Я именно про это.

                    … по части асинхронных библиотек все очень бурно идет на данный момент (из-за только что стабилизировавшегося async/await)…


                    Я тут только в процессе. Учусь и присматриваюсь. Если честно, то я пока не понял насколько это востребовано в моем варианте. Асинхронный код, запускаемый по обработчикам прерываний на bare metall (которые по природе асинхронны) — это вкусно. Я умею такое на С, но что с этим делать на Rust я пока не представляю. Посмотрим…

                    … тут можно локализировать unsafe…


                    Но знаю… Давайте я осторожно скажу, что не уверен. Тема математического доказательства безопасности — не мой конек, но множество архитектур железа наводит на некоторые мысли о том, что не все safe действительно всегда и везде будут безопасны. Впрочем, еще раз — не мой конек. Готов принять на веру что в этой части все сильно лучше, чем кажется.

                    … C и особенно C++ страшны именно количеством UB на каждом шагу.


                    Черт его знает. Может быть это просто уже привычный кактус. И он не так колется. Или просто привык выкусывать мякоть и не попадаться на колючки.

                    А может и наоборот — многолетняя практика ошибок выявила все возможные места, которые могут трактоваться программистами по разному. И узаконила их. Если так, то где гарантия что с ростом популярности и количеством сторонних компиляторов любой язык не обрастет чем-то похожим?

                    … надо просто бенчмаркать, профилировать и следить за тем какой код получается, особых отличий наверное нету?


                    Ну, бенчи и профилировщики в bare metall это редкие гости. Просто в силу проблемности реализации. Это все же не прикладной код под D-Trace'ом.

                    А вот следить за тем какой код получается — это точно. Но тут-то как раз и ловушка. Оставлять unsafe — значить писать как на C. А зачем, если есть С? Ставить safe — разменивать скорость на безопасность, но это же можно и на C. Только там я контролирую процесс, а тут? В лучшем случае баланс сильно смещается в безопасность. Но устроит ли меня такой вариант? Больше того, для кода с safe никто не обещает гарантированного времени выполнения. Т.е. то густо, то пусто… Неприятно. Впрочем, это мои задачи. И это специфика bare metall. Для прикладного ПО, пожалуй, это все не столь критично.

                    Но в целом чем дальше, тем больше спор напоминает спор автомобилистов. Одни кричат, что у нас безопасность и удобство: коробка автомат, ABS, ESP, EBD, BA, на крайний случай ремни и подушки по кругу, вторые кричат что безопасность — это мастерство водителя и возможность тотального контроля поведения машины, который ни одна электронная система никогда не перехватит. По мне спор этот будет длиться еще очень и очень долго. Но рано или поздно победа так или иначе будет за автоматами. Тотальный контроль если и останется, то будет нишевым решением. Так и с языками. Останется С где-нит на самом нижнем уровне. А все, что выше планировщика — все будет на чем-то «безопасном».
                      +4
                      Оставлять unsafe — значить писать как на C. А зачем, если есть С?
                      Чтобы в остальных местах использовать zero-cost safe, где компилятор на стадии компиляции ловит за тебя ошибки (или сразу показывает что твоя архитектура не работает). C таких гарантий не дает просто из-за своей структуры.

                      Ставить safe — разменивать скорость на безопасность
                      Ну вообще далеко не всегда. Те же массивы если обращаться к конкретному элементу — да, будет по умолчанию проверять границы, но если через итератор то этих проверок не будет. Далеко не весь safe дорогой, я бы сказал даже наоборот. Обычно если идет какой то оверхед то для этого уже конкретный тип применяется (условный Rc/Arc) и это явно видно.
                      Впрочем я сам наверное junior в расте, как и в системном программировании в общем.

                      Больше того, для кода с safe никто не обещает гарантированного времени выполнения
                      Ничем не отличается от unsafe кода. У вас либо на уровне логики все хорошо, либо вы вводите дополнительные примитивы для той же многопоточной синхронизации, что в safe что в unsafe. Просто в safe вам никто не даст забыть такой примитив.

                      вторые кричат что безопасность — это мастерство водителя
                      огромное количество CVE состоящие из банальных buffer overflow довольно красноречиво говорит о том что лучше бы эти дела ловил компилятор. Особенно во всяких сложных случаях где структуры завязаны друг на друга. Условно говоря в Rust вам не страшно взять лишний раз ссылку и не бояться что вы забудете что то синхронизировать или освободить, а в C вы бы для страховки сделали лишнюю копию, или того хуже — не сделали бы и получили use after free. Просто потому что в большой программе сложнее следить за целостностью между разными частями.

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

                      В прочем это обычное перечисление плюсов и минусов, каждый сам для себя выбирает. Мне например, (как начинающему «системщику») полезно когда компилятор за ручку держит, да и в целом ощущение почти как в скриптовых языках (удобно и приятно), только без пенальти по производительности (zero-cost).

                      Ну а по поводу железок, да, пока что в Rust экосистема не так развита под embeded (хотя есть working group под это дело), да и для конкретной платформы микроконтроллера поддержка может быть Tier3 поддержка (если вообще присутствовать). Это не значит что не получится скомпилировать под нужную архитектуру, но однозначно придется плясать на каждом шагу.
                        0
                        огромное количество CVE состоящие из банальных buffer overflow довольно красноречиво говорит о том что лучше бы эти дела ловил компилятор.


                        А такая постановка вопроса заставляет нас задуматься в другом ключе: программирование это ремесло или искусство?

                        Я начинал, когда оно было искусством. Как и схемотехника, и многие другие сферы. Потому умом соглашаюсь с Вашими доводами, но сердцем ругаю «серых троечников» от программирования.

                        Посмотрим. Технически практически во всех сферах есть область «ширпотреба» и область «hand maid'а». От пиво- и самогоноварения, через кулинарию и Hi-Fi до авиастроения. Наверное, в этом что-то есть. И, вполне возможно, что рано или поздно дойдет очередь и до программирования.
                          +3
                          Я вижу в элитарности программирования вред. Машины на автопилоте как и умные компиляторы потенциально лучше человека справляются (и работают быстрее).

                          Я думаю в области программирования и так много задач/проблем чтобы фокусироваться на таких мелочах которые компилятор вполне способен сам отловить.

                          Тут еще интересный эффект что компилятор Rust меня по сути учит как безопасно и правильно программировать. В свое время лет в 14 я просто не осилил C++ на нормальном уровне ибо там было слишком много мудреных проблем и криптованных ошибок.

                          Ну и когда продакшен стоит уязвимый из-за hearthbleed который допустили вроде как опытные разработчики (да и всем миром проглядели в исходниках) становится совсем печально.
                            –1
                            Стоит ли это понимать, как роспись в собственном бессилии? Ведь компилятор тоже не с небес спустился. Его тоже кто-то написал. И кто-то проверил. Стоит ли так безоговорочно верить ему?

                            А единственный компилятор, который реально безопасно программировать учит — это компилятор с ассемблера. При чем учит так, как кошмарят по поводу плавания: бросили за борт — выплывешь, значит научишься, а нет… не судьба. Безопасно писать можно только своими руками пощупав все костыли, которые только возможны. Даже С такой свободы не дает (и ответственности не налагает).

                            Так что по сути ситуация как сейчас. Кто-то пишет драйвера, а кто-то Web-приложения. И между ними всякие браузеры гуляют. Вот и разделение. Для низа C и Assembler. Дай бог Rust'у туда влезть, но… Без боя не прокатит — это вам не прикладников теснить, тех которые на голом C по голому железу да без malloc() не могут. Для серединки — ну тот же Rust и какой-нить Go. Тут С++ тоже повоюет, но… Я бы сказал, есть шансы его подавить… Ну а в вебе скриптовые языки. Тут без вариантов.

                            Тут уж Вам решать какой слой элитарным обзывать. Я, кстати, про элитарность ничего не говорил. И даже мысли не было. Мне все равно кого элитой назовут. Просто чем ниже, тем большая подготовка нужна. Там возможностей больше, но и требования выше, и ответственность серьезнее. Я это искусством называл, а не элитой. Разные понятия. Особенно в современном мире.

                            Мне (не?) повезло — я всегда в самом низу. В лучшем случае (контроллер) весь проект мой, в худшем (BSP, драйвера, подсистемы) сверху гора прикладников, которых не волнует медленная периферия, необходимость синхронизации данных или что-то еще. Им нужны данные в любое время или как минимум четкий сигнал об их отсутствии. Ну и не занимать процессор. А то до тактов их скриптовые языки жадные.

                            А безопасность Rust… Не получилось бы с ней как с небезизвестной Spectre. Сначала все прикрыли, а потом ужаснулись провалу в производительности и призадумались — а справедлива ли цена? И это тот еще вопрос. Нет по нему консенсуса.
                              +2

                              Только компилятору вам придётся верить в любом случае, потому что у С тоже есть компилятор.

                                –1
                                Ну вот… Конечно. Только давайте все же не сравнивать эти два компилятора. Особенно по части обращения с данными. Ибо C творит с ними ровно то, что может процессор. Потому и не возражает против переполнений или чего-то похожего, и система кодогенерации у него крайне простая. А вот Rust напротив вносит некоторое количество проверок усложняя код. Да еще как мне тут рассказывают для разных случаев разные проверки. Мало того что в каждой из них потенциально проблемы, так еще возможность выбора «не той системы».

                                И еще момент — в силу простоты реализации компилятор С легко проверяется изучением дизассемблированоого текста. Можно ли так же на Rust'е? Понятен ли будет его ассемблерный код (особенно тому, кто ратует за него, ибо даже C сложно)? Сработает ли этот факт на повышение надежности? Мое мнение — однозначно нет.
                                  0
                                  Потому и не возражает против переполнений или чего-то похожего, и система кодогенерации у него крайне простая.

                                  Ага, особенно против знаковых переполнений.


                                  Вы что, собираете код с -O0, а оптимизации включать уже нельзя-нельзя?


                                  Ибо C творит с ними ровно то, что может процессор.

                                  Как и любой другой язык, тащем.


                                  А вот Rust напротив вносит некоторое количество проверок усложняя код.

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


                                  И еще момент — в силу простоты реализации компилятор С легко проверяется изучением дизассемблированоого текста. Можно ли так же на Rust'е? Понятен ли будет его ассемблерный код (особенно тому, кто ратует за него, ибо даже C сложно)? Сработает ли этот факт на повышение надежности? Мое мнение — однозначно нет.

                                  Мне дизассемблер плюсов (с оптимизациями, конечно) далеко не всегда понятнее дизасма идриса, скажем.

                                    –1
                                    Мы не первый раз встречаемся с Вами в комментарих. Может стоит попытаться расставить реперные точки? Давайте сначала.

                                    Когда нацисты хватали коммунистов, я молчал: я не был коммунистом.
                                    Когда они сажали социал-демократов, я молчал: я не был социал-демократом.
                                    Когда они хватали членов профсоюза, я молчал: я не был членом профсоюза.
                                    ...


                                    Так вот — пока Rust воевал с С++ и Go, я молчал: это не моя поляна.
                                    Это поляна прикладников, они пусть с ней и разбираются. Меня вопрос заинтересовал исключительно после комментария Грега о том, что Rust вполне возможно появится как инструмент кодогенерации в ядре. Вот тут мне пришлось пошевелиться и посмотреть что это за «зверь неведомый», «с чем его едят» и «стоит ли его бояться»? Решать не мне, конечно, но… Пока я вижу только проблемы. Везде. От кросскомпиляции до форматов данных. Нет, это работает. Но диапазон сильно уже, чем минимально необходимый. Потому ждем. Слишком рано.

                                    Теперь отдельно про кодегерацию и дизассемблерный листинг. Начнем с простого. Конечно, не -O0. Ну что Вы в самом деле. Базовая настройка всегда -Os. Но весь прикол в том, что включать, допустим, -O3 в подавляющем большинстве случаев приведет к замедлению, а не к ускорению. И как раз дамп это прекрасно показывает. Причины этого я напишу чуть ниже. Просто чтобы не мешать в одну кучу мух и котлет. Каждая строка моего C кода транслируется в одну-две (редко больше) ассемблерные инструкции. При чем в подавляющем большинстве случаев я знаю какие именно инструкции будут, и как повлияют настройки оптимизации на поведение компилятора. Больше того, я точно знаю что в подавляющем большинстве случаев никак не повлияют. Что бы я не поставил после -O. А в меньшинстве, как раз приходится бороться с «излишне умным» компилятором. Чаще всего путем #pragma GCC optimize («O0»)

                                    Теперь про переполнения. Знаковые, беззнаковые — уже детали. Да, у процессора есть флаг, позволяющий данную ситуацию отловить. Т.е. если спуститься глубже, то можно это дело накрыть. И да, язык С не транслирует это выше. Переполнение и переполнение. Ну что теперь…

                                    Казалось бы криминал-криминал. Но ведь нет. И знаете почему нет? А ровно потому, что мастер знает свой инструмент. Даже люди старой закалки, писавшие на ассемблере, практически никогда не проверяли флаг переполнения. Тому есть несколько причин. Первая — мастер, в отличие от ремесленника, не решает задачу, а проектирует систему. А в этом случае размерность типов данных выбирается такой, чтобы переполнений не возникало. Или они были строго контролируемы. Причина, вызывающая переполнение, в подавляющем большинстве случаев это недостаточная фильтрация входных данных. Так вот всегда бороться надо с причиной, а не со следствием. Вторая причина в том, что информация о случившимся переполнении на уровне процессора практически бесполезна. Хорошо, случилось переполнение. И как прикажете на него реагировать? Направлять программу по другому пути, где предусмотрены большие размеры? Падать с ошибкой? Или что? Вот потому-то и важно не садиться кодить сломя голову, а садиться и думать. О системе. О том как она будет работать и как будет себя вести в случае неадекватных входных данных. Опять же — скриптовым языкам это может быть и полезно, но всему что транслируется в машинный код информация о переполнении… Не думаю.

                                    А вот теперь, сказав о необходимости проектирования системы и контроля входных параметров можно вернуться к дополнительным проверкам в Rust. Впрочем, я уже все сказал. Потому особо задерживаться здесь не будем. Контроль входных данных (как минимум на самом низком уровне) — однозначно ответственность автора. Только так, и никак иначе.

                                    Теперь про дизассемблер оптимизированных плюсов. Тут я сразу отмажусь тем, что актуального состояния не знаю. Может быть. Но плюсы и более высокие языки в дизассемблере… Впрочем, Вы ж не ломаете собственный код. У Вас же есть отладочная информация. Что там может остаться непонятным? Для меня загадка. На рубеже 2000-2010 годов (эпоха shareware софта) я развлекался сломом и написанием кейгенов. Так со временем даже Delphi (не к ночи будь помянут) в дизасемблере становился понятным. А уж плюсы просто с листа читались. Но еще раз — актуальным состоянием дел я не владею. Потому верю на слово, и, если хотите, сочувствую. Мне бы без понимания того как именно работает написанный мной код было бы очень тяжело.

                                    И напоследок. Скажите, а Вы осознанно ставите знак равенства между C и C++? Если это и родственники, то дальние. В лучшем случае двоюродные братья. По мне не очень разумно их сливать в один чан. Конечно, офисный пакет класса LibreOffice или САПР класса AutoCAD писать на С не будешь. Для этого есть плюсы. Но и у C есть своя ниша. Конечно, одну и ту же задачу можно решить разными инструментами. Но несколько негоже оперировать мечем, колоть дрова скальпелем или забивать гвозди микроскопом. Еще хуже жаловаться при этом на инструмент. Разве нет?

                                    А теперь вернемся к началу статьи. В битве прикладных языков я занимаю позицию наблюдателя. А системная составляющая Rust вызывает у меня (и не только у меня) вполне обоснованные вопросы. И только.

                                    И еще. Я старался максимально убрать любой сарказм и любые подколки. Если что осталось, прошу простить. Я не со злаю
                                      +3
                                      Каждая строка моего C кода транслируется в одну-две (редко больше) ассемблерные инструкции. При чем в подавляющем большинстве случаев я знаю какие именно инструкции будут, и как повлияют настройки оптимизации на поведение компилятора.

                                      И функции у вас не инлайнятся, и циклы не разворачиваются-сворачиваются, и автовекторизации не происходит, и чтения из одной и той же переменной не оптимизируются? С pointer provenance что ваш компилятор делает?


                                      Теперь про переполнения. Знаковые, беззнаковые — уже детали.

                                      Нет. Стандарт C запрещает знаковые переполнения, и компилятор может свернуть if (a + b < a) как проверку на переполнение в false.


                                      Да, у процессора есть флаг, позволяющий данную ситуацию отловить.

                                      Как, кстати, вы к нему на С обратитесь?


                                      Вдруг сейчас выяснится, что С тоже не подходит для системного программирования.


                                      Контроль входных данных (как минимум на самом низком уровне) — однозначно ответственность автора.

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


                                      Например, достаточно мощный язык может позволить иметь только одну функцию для доступа к элементам массива, требующую проверки доказательства, что вы не вылезете за его границы. Можете построить доказательство статически, например, если пробегаете по массиву в цикле? Супер, компилятор его вырежет при компиляции. Не можете? Придётся проверять в рантайме (но вам бы и так пришлось). Впрочем, эту проверку можно будет сделать один раз и сохранить результат, доказательства — это значения, в конце концов. Или проверить один раз, что максимальный индекс из серии индексов подходит, а для всех остальных ничего проверять уже не нужно.


                                      Таким образом, вы получаете и производительность (когда вы заранее знаете, что всё в порядке), и безопасность. Это ли не чудо?


                                      С одной стороны, конечно, это всё не про раст. С другой — это показывает, что безопасность не обязательно противоречит производительности.


                                      Вот потому-то и важно не садиться кодить сломя голову, а садиться и думать.

                                      Учитывая предыдущий кусок — так тут тоже все эти языки помогают. Потому что себе я не доверяю, не доверяю тому, что буду поддерживать 100% внимательность всё время, что не напишу хрени с недосыпа, и тому подобные вещи (и цитата рядом про Пиркса — она про другое). А если машина мне скажет «чувак, тут бы доказательство непереполнения/валидного индекса/етц», а у меня его под рукой нет, и что делать, если индекс таки некорректный, непонятно, то да, придётся садиться и додумывать. Но лучше это делать после ругани компилятора, а не заказчика.


                                      У Вас же есть отладочная информация. Что там может остаться непонятным?

                                      У меня почти всегда при отладке <value optimized out>, даже с дебаг-символами.


                                      А вообще ещё бывает интересно, в какой именно код оно там соптимизировалось. Ну, когда вы выжимаете последние наносекунды (да, это делают не только в эмбеддеде, но и на многоголовых многогигагерцовых ксеонах). Или когда отлаживаете какой-то особо интересный баг, вылезший, возможно, из ещё одного UB.


                                      И напоследок. Скажите, а Вы осознанно ставите знак равенства между C и C++? Если это и родственники, то дальние.

                                      Потому что сравнивать раст разумнее с плюсами, ИМХО.


                                      Кстати, забыл ещё в прошлый раз сказать — стандарт С занимает страниц 150 по части объяснения семантики языка, далеко не строго формального, с возможностью неоднозначных толкований и необходимостью делать от одного до нескольких десятков прыжков, чтобы выяснить, как конкретно работает какой-то кусок стандарта (правда, о последнем я скорее сужу по плюсам, но у него стиль стандарта отличается не принципиально). Ядро системы формальных доказательств описывается на одной странице A5 предельно строгим и формализованным набором правил. Я бы не сказал, что написание компилятора С — такая уж простая задача. Тайпчекер и базовый интерпретатор для CoC можно написать за выходные, для С — не думаю.

                                        0
                                        Ядро системы формальных доказательств описывается на одной странице A5 предельно строгим и формализованным набором правил
                                        а сколько занимает описание нотаций этого А5?
                                          0
                                            0

                                            А термины, используемые в стандарте С, знакомы с рождения?


                                            А вообще так как теория типов — довольно фундаментальная вещь, опирается она на не так много вещей.

                                            0
                                            Как, кстати, вы к [флагу переполнения] на С обратитесь?

                                            __builtin_add_overflow() и компания.

                                              0

                                              Не нашел такой функции в тексте стандарта.

                                                0
                                                Спасибо. Вот значит — все уже написано. Во всяком случае в GCC. Скажу честно — не знал.
                                                0
                                                И функции у вас не инлайнятся...


                                                Вот она моральная проблема. С одной стороны надо свой код показать, с другой именно то, что показать можно не очень показательно. Да и хвастовством отдает… Ладно, посмотрите драйвер и подсистема. Ну, и чуть более сложный пример и прочие драйвера рядом.

                                                А потом скажите — много ли свободы остается для оптимизатора? И уж поверьте на слово — в контроллерах примерно так же. Рад бы показать, но все проекты коммерческие — и в открытую не лежат. Когда одна из базовых идей как раз минимизация кода (и тактов) оптимизатору остается разве что платформо-специфичные трюки проделывать. Он, собственно, именно этим и занят. На уровнях выше никакого. Не та область, где ему развернуться можно.

                                                Как, кстати, вы к нему на С обратитесь?
                                                Вдруг сейчас выяснится, что С тоже не подходит для системного программирования.


                                                А вы читали то, что я выше писал? Я же четко и однозначно написал:
                                                И да, язык С не транслирует это выше. Переполнение и переполнение. Ну что теперь…

                                                И даже пояснил, почему это не важно от слова совсем. Разве нет?

                                                Но раз пошли наезды на мои любимые инструменты, то могу за них постоять. Для начала «кто сам без греха пусть первым бросит камень». Что-то я навскидку не припомню языков, которые бы так умели. Опять же — причина выше обозначена. Во вторых, я не знаю реализовано ли это в Rust (не дошел), но беглый поиск приводит к этой статье. И если так, то при необходимости я легко сделаю это «как в Rust» банальной ассемблерной вставкой или даже статически линкумемой библиотечкой. В обоих случаях, кстати, может находиться и банальная заглушка ежели вдруг найдется архитектура такую функциональность не поддерживающая. А вот как будет выкручиваться на таких архитектурах Rust? Неужели переписывать ассемблерные команды математики?

                                                Нужна ли эта функциональность в стандарте языка? Мое мнение — абсолютно не нужна. Во всяком случае в стандарте языка С точно нет.

                                                Потому что себе я не доверяю, не доверяю тому, что буду поддерживать 100% внимательность всё время, что не напишу хрени с недосыпа, и тому подобные вещи...


                                                Странно, мы исходим из одних и тех же предпосылок, но приходим к разным выводам. Мне кажется, что «семь раз отмерь, один отрежь» более правильный подход. Не надо писать «хрени с недосыпу». От слова совсем. Есть множество работ, даже у программиста, которые не требуют головного мозга. Достаточно спинного (читай инстинктов). Вот их с недосыпу и надо делать. Они всегда откладываются, ибо тупые и неинтересные, но когда мозг не работает — для них самое время.

                                                Впрочем, поправьте если я ошибаюсь. Вы же больше прикладник? Или пришли в системщики из прикладников? Математические обоснования, необходимость писать во что бы то ни стало чтоб успеть к очередному дедлайну — это скорее оттуда. Я пришел из схемотехников. А там суетность всегда приводит к выпуску из устройства главной составляющей — волшебного дыма. Потому наш лозунг «Сейчас медленно спустимся с горы и, хм..., разберемся со всем стадом».

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

                                                Только не сочтите это за наезды или что-то такое. Мир большой. В нем место найдется любому подходу.

                                                Потому что сравнивать раст разумнее с плюсами, ИМХО.


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

                                                Вы меня «валите» теми плюсовыми заморочками, которые меня или совсем не касаются, или касаются очень опосредовано. И по большому счету мне и возразить нечего — я просто с этим никогда не сталкивался.

                                                Но я согласен с Вами. Если Rust и конкурент (с оговоркой, конечно, — в современном мире), то конкурент плюсам. Потому с ним можно поиграться, как с забавной игрушкой, но до применения в железе ему еще далеко. Впрочем, вполне себе предвидя Ваши возражения, все же напишу что и плюсов это тоже касается. С одно маленькой поправкой. Им уже далеко.

                                                Конечно, все написанное исключительно мое субъективное мнение. Ни коим образом не претендующее на звание «единственно верного и идеологически выдержанного».
                                            0
                                            Мало того что в каждой из них потенциально проблемы, так еще возможность выбора «не той системы».
                                            Выбор системы в Rust обычно либо явный либо оптимальный. А базовые проверки в no_std можно и ручками разглядеть разок чтобы знать в дальнейшем как они под капотом выглядят. Я уже не говорю о том что есть и unsafe варианты которые дают вам тоже что и в C. Надо perfomance critical — делайте в конкретном месте unsafe. В остальных местах компилятор поможет отловить банальные но очень больные ошибки.

                                            Потому и не возражает против переполнений или чего-то похожего
                                            Rust тоже не возражает в release режиме, а вот в дебаге он вам покажет что вы не правы.

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

                                            Да еще как мне тут рассказывают для разных случаев разные проверки. Мало того что в каждой из них потенциально проблемы, так еще возможность выбора «не той системы».
                                            Строго говоря с ними разобраться и «выучить» их быстрее чем разбираться со всеми UB в разных компиляторах на разных платформах C.

                                            Понятен ли будет его ассемблерный код
                                            А почему он должен так сильно отличаться от C?
                                            godbolt.org/z/am8Bwf
                                            godbolt.org/z/wp6wpu

                                            Сработает ли этот факт на повышение надежности? Мое мнение — однозначно нет.
                                            Но компилятор ловит больше ошибок чем компиляторы C, разве это не повышает надежность?
                                              0
                                              А почему он должен так сильно отличаться от C?
                                              godbolt.org/z/am8Bwf
                                              godbolt.org/z/wp6wpu


                                              А если примеры посложнее, да с safe массивами? А еще замечательный пример — реализация кольцевых буферов, где переполнение чуть ли не основная фишка. Впрочем, я уже ответил выше.
                                                +1
                                                Строго говоря с ними разобраться и «выучить» их быстрее чем разбираться со всеми UB в разных компиляторах на разных платформах C.
                                                «undefined behavior» — категория стандарта языка, он не зависит от платформ и компиляторов.
                                          –1
                                          Я вижу в элитарности программирования вред. Машины на автопилоте как и умные компиляторы потенциально лучше человека справляются (и работают быстрее).

                                          Это мне напоминает известную дискуссию с Пирксом в «Ананке» Лема:

                                          — Командор… — Хойстер говорил тихо и со странной медлительностью, будто осторожно подбирал слова. — Вы ведь ориентируетесь в ситуации, правда? Два следующих корабля того же типа, с той же системой управления сейчас находятся на линии Земля — Марс; «Арес» будет здесь через шесть недель, но «Анабис» — всего через девять дней. Не говоря уж о том, к чему нас обязывает память о погибших, мы имеем еще большие обязательства перед живыми. За эти пять часов вы, несомненно, уже обдумали все, что произошло. Я не могу заставить вас говорить, но очень прошу сообщить нам, к каким выводам вы пришли.
                                          Пиркс почувствовал, что бледнеет. С первых же слов он понял, что хочет сказать Хойстер, и вдруг его охватило странное ощущение ночного кошмара: ожесточенное, отчаянное безмолвие, в котором он сражался с безликим противником и, убивая его, словно погибал с ним вместе. Это длилось мгновение. Он овладел собой и взглянул прямо в глаза Хойстеру.
                                          — Понимаю, — сказал он. — Клайн и я — это два разных поколения. Когда я начинал летать, автоматика подводила гораздо чаще… Это накладывает отпечаток на все поведение человека. Думаю, что Клайн… доверял автоматам до конца.
                                          — Клайн думал, что компьютер лучше разбирается в деле? Считал, что он сможет овладеть ситуацией?
                                          — Может, он на это и не рассчитывал… а только думал, что если компьютер не справится, то человек тем более.
                                          Пиркс перевел дыхание. Он все же сказал, что думал, не опорочив при этом младшего собрата, уже погибшего.
                                          — Как по-вашему, была возможность спасти корабль?
                                          — Не знаю. Времени было очень мало. «Ариэль» почти потерял скорость.
                                          — Вы когда-нибудь садились в подобных условиях?
                                          — Да. Но в маленькой ракете — и на Луне. Чем длиннее и тяжелее корабль, тем труднее восстановить равновесие при потере скорости, особенно если начинается крен.
                                          — Клайн вас слышал?
                                          — Не знаю. Должен был слышать.
                                          — Он взял на себя управление?
                                          Пиркс хотел было сказать, что все это можно узнать по лентам, но вместо этого ответил:
                                          — Нет.
                                          — Откуда вы знаете? — это спросил Романи.
                                          — По контрольной табличке. Надпись «Автоматическая посадка» светилась все время. Она погасла, лишь когда корабль разбился.
                                          — А вы не думаете, что у Клайна уже не оставалось времени? — спросил Сейн. Его обращение выглядело подчеркнутым — ведь они были на «ты». Словно бы между ними обозначилась некая дистанция… может, враждебность?
                                          — Ситуацию можно математически промоделировать, тогда выяснится, были ли шансы, — Пиркс старался говорить конкретно и по-деловому. — Я этого знать не могу.
                                          — Но когда крен превышает 45 градусов, равновесие уже невозможно восстановить, — настаивал Сейн. — Ведь верно?
                                          — На моем «Кювье» это не совсем так. Можно увеличить тягу сверх установленных пределов.
                                          — Перегрузки больше двадцатикратной могут убить.
                                          — Могут. Но падение с высоты пяти километров не может не убить.
                                          На том и окончилась эта краткая дискуссия. Под лампами, включенными, несмотря на дневную пору, плоско стлался табачный дым. Все курили.


                                          Собственно, сейчас тенденция уже видна по некоторым пилотам гражданской авиации, которые не способны на ручном управлении, без помощи электроники посадить лайнер. Хотя они и должны в общем-то это уметь, для этого они там и сидят.
                                            –1
                                            — Перегрузки больше двадцатикратной могут убить.
                                            — Могут. Но падение с высоты пяти километров не может не убить.


                                            Я бы плюсанул, если бы мог. Хороший пример, и главное в месту. Вопрос только в том смогут ли это понять. А то «все это уже было в симпсонах» воспринимается, а вот «все это уже было у Лема, Хайнлайна и многих других» как-то не очень. Да и не только среди фантастической литературы, к слову.
                                              0

                                              Вот только программист — не пилот, а компилятору нет необходимости работать в реальном времени.

                                                –1

                                                В данном случае это не столь важно. Речь про позицию, так сказать, принципиального неосиляторства "ничего не хочу знать, ничего не хочу уметь, не осилил и горжусь этим, ведь компилятор все сделает лучше меня, а если нет, то это плохой компилятор" и её последствия.

                                                  0

                                                  Так последствия-то как раз разные.

                                                    0

                                                    Это зависит. Проблему с MCAS у Боинга помните? Не осилили одни (причём вовсе не в плане работы со страшными указателями), не осилили другие (не сумели отключить художества первых), в результате катастрофа и гибель людей.

                                                      0

                                                      Помню я проблему с MCAS. Я даже помню что проблема там не в коде была.


                                                      Но даже если представить аналогичную ситуацию, но исправимую кодом — я считаю, что с хорошим компилятором у меня останется больше свободных когнитивных ресурсов для её своевременного выявления.

                                                        0

                                                        Насколько я помню, в каком-то смысле как раз в коде — чрезмерное доверие к показаниям определённого датчика, когда он начинал "чудить", начинались проблемы. Всё по канонам "машина не может ошибаться".

                                                          0

                                                          Если рассмотреть ситуацию с точки зрения программиста MCAS — то у него не было никакой альтернативы этому самому доверию, поскольку датчик-то ему дали всего один. Ошибся тот, кто "завёл" на модуль автопилота всего 1 датчик. И это решение за него точно не компьютер принял.

                                                            0

                                                            Если бы я был тем программистом MCAS, я бы отказался реализовывать заведомо ненадежный алгоритм, более того — уведомил бы о происходящем компетентные органы. Одно из двух — либо программист считал, что все ОК, и одного датчика достаточно, либо он не осознавал своей ответственности за потенциальные сотни трупов и ему было пофиг. Что из этого хуже — трудно сказать.

                                                              0

                                                              Ещё одна нить runaway stabilizer иди.


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


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

                                                                0

                                                                Да, я знаю о такой точке зрения, и считаю её обоснованной, тем более что как минимум один из экипажей осилил данную проблему ещё до первого падения (правда, на его сообщение о данном происшествии особого внимания не обратили). Тем не менее, считаю, это не снимает доли ответственности с разработчиков.

                                                                  0
                                                                  не потому, что датчик или MCAS чего-то там, а потому, что плохо обученные пилоты не смогли парировать совершенно восстановимый и некритичный отказ


                                                                  А можно ссылку на Ваши источники? Я плохо знаю тему и вынужден верить тому, что пишут. Но пишут-то как раз обратное. Что MCAS работает только при ручном управлении, и именно она вместо неотключаемого помощника превратилась неотключаемого врага. Бороться с автоматикой в таком случае тяжело. Все равно что пытаться широким плечом несущийся камаз остановить…

                                                                  Но вернувшись к теме. Это как раз ошибка проектирования. Не спас бы ситуацию тот же ошибочно спроектированный код, но написанный на Rust. Вот и думайте где безопасность, а где ее иллюзия.

                                                                    +2

                                                                    Она настолько же неотключаема, насколько и любой другой автоматический механизм управления триммером. Да, чтобы триммер переложить после отключения автоматики, придётся приложить некоторые усилия (в прямом физическом смысле), но даже попыток это сделать не было.


                                                                    Источник — сканы РЛЭ боинга, которые тут приводили в соответствующих тредах полгода-год назад, плюс некоторые представления о том, как работают самолёты, сформированные любопытством к авиации, на которые я сослаться, увы, не смогу.


                                                                    А на Rust бы код точно не спас, да.

                                                                      0
                                                                      Бороться с автоматикой в таком случае тяжело. Все равно что пытаться широким плечом несущийся камаз остановить…

                                                                      Аналогия некорректна: КАМАЗ человека просто раздавит, а у команд пилота перед автоматикой приоритет.

                                                                    0

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

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

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

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

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

                                                                            0
                                                                            Можно окольными путями к очередному UML-генератору вернуться.
                                                                              +2

                                                                              Но только UML ничего не верифицирует.

                                                                                0
                                                                                Возможно, начнёт. Он изначально и не генерировал.
                                                                              0
                                                                              Значит, надо брать более выразительный язык, могущий отлавливать и логические ошибки.


                                                                              Хм. Программы, пишущие программы. Роботы, проектирующие и изготавливающие других роботов.

                                                                              Я прозевал новый виток эволюции, на котором люди не нужны?
                                                                                0

                                                                                Зачем же сразу программы пишущие программы? Просто программы, позволяющие формально проверить выполнение тех или иных условий вот прям в коде. «Единичный отказ одного датчика не приведёт к отказу MCAS» как пример такого утверждения.

                                                                                  –2
                                                                                  Вопрос только в том откуда компилятор должен знать о том, что требуется проверять? И что будет критерием отказа датчика? И как действовать в такой ситуации? Не получится так, что описание критериев тестирования встанет более сложной задачей чем их реализация? Отменит ли прохождение теста натурные испытания (и если нет, то зачем тест)?

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

                                                                                  Вот и получается, что есть целый список вопросов, на которые сознательно или нет, но забили при проектировании. Как результат неверные действия тех же пилотов. Им никто не сказал о неисправном датчике или некорректно работающей подсистеме. Как результат ее никто и не отключал, а фактически боролся с ней. И даже помолчим о том, что процедура отключения данной подсистемы совсем не тривиальна.
                                                                                    0
                                                                                    Вопрос только в том откуда компилятор должен знать о том, что требуется проверять?

                                                                                    Вы ему скажете. У вас же где-то, пусть даже на бумаге, эта спека есть?


                                                                                    И что будет критерием отказа датчика? И как действовать в такой ситуации?

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


                                                                                    Не получится так, что описание критериев тестирования встанет более сложной задачей чем их реализация?

                                                                                    Почти уверен, что получится. Но критерии вам описывать всё равно надо, хоть в каком-то виде.


                                                                                    Отменит ли прохождение теста натурные испытания (и если нет, то зачем тест)?

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


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

                                                                                    Опять во всём капитализм виноват. Я обожаю об этом говорить, но, боюсь, тут это будет оффтопом.

                                                                                      –1
                                                                                      Вы ему скажете.

                                                                                      :))) То есть по факту человек должен объяснять компилятору, что именно компилятор потом должен у человека проверить. Классическая проблема «Quis custodiet ipsos custodes?» Сейчас неосилятор забывает проверить, потом будет забывать объяснить компилятору, что тому надо будет у него проверить, потом надстроят еще следующий уровень, и он будет забывать уже объяснить какому-нибудь верификатору правил для компилятора, что именно надо проверить… Замкнутый круг.
                                                                                        0

                                                                                        Но так вы хотя бы можете иметь одного сеньора со знанием кока/идриса/етц на команду, аутсорсенную в Дели, который пройдётся по сигнатурам топ-левел-функций и убедится, что спеке оно соответствует. А иначе надо весь код читать и ручками проверять.

                                                                                          0
                                                                                          Ну, не знаю. Злые языки, например, писали, что Боинг заказывает софт индийским аутсорсерам, но та же проблема с MCAS (я сейчас говорю в техническом смысле) возникла же не из-за аутсорсеров, а из-за того, что какой-то боинговский сеньор помидор счел нормальным заказывать и принимать код, который заведомо имеет single point of failure в виде этого злополучного датчика. Ну и чем бы тут помогли спеки, если он их сам и составил в таком вот ключе?
                                                                                            0

                                                                                            Никак. Никто ж не говорит, что спеки ­— серебряная пуля.

                                                                                        0
                                                                                        У вас же где-то, пусть даже на бумаге, эта спека есть?


                                                                                        Так, а вот с этого момента стоп. Если это было в спецификации, но не реализовалось в железе — вопрос как его принимали? И пофиг кто и что там проверял — компилятор, отк, натурные тесты… Но проблема в том, что судя по публикациям в спецификации такого и не было. И вообще фактически новый самолет был выпущен под видом модификации существующей модели. С крайне сокращенной программой испытаний.

                                                                                        Да, это та самая ошибка проектирования системы. Когда критически важные моменты проходят мимо ТЗ, спецификаций, приемки. И никакой компилятор это исправить не может. А если это есть — по по совести пофиг на компилятор. Хоть на бейсике пишите — лишь бы требованиям спецификации соответствовало.

                                                                                        Разве не так?
                                                                                          0

                                                                                          Нет, не пофиг на компилятор.


                                                                                          Потому что когда у вас компилятора нет, то вы можете ошибиться дважды: в написании спеки и в проверке, что ваш код её удовлетворяет. Когда у вас верифицирующий компилятор есть, то ошибиться вы можете только при написании спеки.

                                                                                            –3
                                                                                            — Кто сам без греха пусть первым бросит камень.

                                                                                            Если ошибка на уровне спецификации, то какой смысл в том что ее гарантированно реализовали? Да и где гарантия того, что верифицирующий компилятор не пропустит ошибки? Вы можете это гарантировать и взять на себя ответственость за последствия? Я готов отвечать за свои код. Но не за код компилятора. Будь он трижды сертифицированным.
                                                                                              0

                                                                                              Я перестал понимать, за какой тезис вы топите. «Компилятор не панацея»? «От формальной верификации нет толку»?

                                                                                                –3
                                                                                                Я же писал. Я топлю исключительно за грамотное проектирование. Начиная с ТЗ, продолжая реализацией и заканчивая контролем. И топлю против перекосов во всем остальном. А компилятор в грамотно спроектированной системе вещь не главная.

                                                                                                Но так как я все же программист, а не менеджер, согласующий ТЗ, и не начальник отдела контроля качества, то отвечать готов за тот этап, который между ТЗ и контролем. Т.е. за реализацию. А тут как раз компилятор. И поскольку за изделие отвечать мне, а не разработчикам компилятора, то я просто обязан знать как именно работает код. А тут градация простая — если код на assembler'e знаю абсолютно. Но это уж очень специфический язык. Как острый перец. Приправой необходим, а вот как основное блюдо… Дальше C. Опять же — знаю. Любой код на С автоматом переводится на ассемблер прямо в голове. Единственное от чего я избавляюсь — так это от чисто ассемблерных заморочек (контроль стека, сохранение регистров). Дальше идут C++ и Rust. По плюсам — до определенного уровня я знаю что и как делает компилятор. И мне это не очень нравится. Уж очень не гуманно в плане памяти (расплата за универсальность). Потому я редко (читай никогда) пишу на плюсах. А навык, который не развивается со временем утрачивается. Ну и Rust… По моим ощущениям он пытается усидеть на двух стульях (как минимум — тут вон еще статья была и про веб-разработку на Rust). C unsafe он ведет себя как C, а с safe еще хуже чем плюсы.

                                                                                                А по итогу получается так: при наличии нормально написанного ТЗ и понятных критериев контроля качества кода мне важнее понятность C, нежели безопасность Rust. Но еще раз — у меня речь идет в основном о контроллерах и системном ПО уровня ядра. Вполне допускаю, что в мире прикладного ПО все обстоит и по другому.
                                                                                                  0

                                                                                                  Естественно, главная вещь — спека. А при наличии этой спеки следующая по важности вещь (в идеальном мире, где люди в курсе высшего образования изучают не только диффуры, но и основы лямбда-исчисления, матлогики и теории доказательств) — компилятор, способный проверить код на соответствие этой спеке. Ну, это мой тезис, по крайней мере.


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

                                                                                                  А почему вы останавливаетесь на этом уровне? Почему вы не смотрите, как работает микрокод вашей железки (если он есть)? Почему вы не смотрите на принципиальную схему вашего процессора/контроллера/что у вас там? Ведь отвечать вам, а не разработчикам контроллера.


                                                                                                  Дальше C. Опять же — знаю. Любой код на С автоматом переводится на ассемблер прямо в голове.

                                                                                                  Можете удержать модель языка и гарантировать поведение компилятора с учётом всех UB? Снимаю шляпу. Я так не могу, мне не хватает памяти, внимательности и времени.


                                                                                                  Уж очень не гуманно в плане памяти (расплата за универсальность).

                                                                                                  А что в плюсах ест память?


                                                                                                  А то я тоже пишу всякие быстрые вещи на плюсах, где тоже важна память (но не потому, что микропроцессор, а потому, что наносекунды важны, и там уже всякие когерентности кешей, false sharing, TLB, про ограниченность или даже существование которого все всегда забывают, все дела), и, ну, не заметил.


                                                                                                  А по итогу получается так: при наличии нормально написанного ТЗ и понятных критериев контроля качества кода мне важнее понятность C, нежели безопасность Rust.

                                                                                                  Ну вот об этом и речь. О себе я не такого хорошего мнения, и я знаю, что реализация Rust/Idris/etc будет более качественной, чем написанный мной код.

                                                                                                    –2
                                                                                                    А почему вы останавливаетесь на этом уровне?


                                                                                                    А хороший вопрос. А не останавливаюсь. В разработке схемы я принимаю участие. Я ее не рисую, но без моего одобрения схема не пойдет в трассировку. Если надо то и трассировщику могу рекомендации выдать. Но это, как правило, лишнее. И даже косяки монтажа я просматриваю. Еще раз — я пришел из схемотехников. Тут мой дом родной. И разработчики очень часто со мной консультируются. А временами даже просвещать приходится. Но все же авторы схемы и платы они, а не я. Я проверящий. Микрокода в контроллерах я не видел. Вроде он там и не нужен. А вот как реально внутри устроен контроллер (какие схемотехнические решения скрываются за сокращениям ALU, VIC/NVIC и иже с ними) я себе вполне представляю. В большом ПК уже нет, но в контроллере вполне. Как следствие есть довольно внятное представление о том, как электроны в изделии бегут от плюса к минусу (а на самом деле наоборот), и как на этот бег влияет моя программа. Может быть именно поэтому и не хочется уходить на языки выше С и терять это понимание.

                                                                                                    А что в плюсах ест память?


                                                                                                    А просто у нас очень разные представления о «есть память». На вскидку мне тяжело даже от VMT (она же vtable). И прочих капелек, из которых собираются литры. А у меня всего ведро. Хорошо когда литров на 18, а не на 3.

                                                                                                    О себе я не такого хорошего мнения...


                                                                                                    Интересный способ поинтересоваться не жмет ли мне корона. На самом деле даже не знаю что и сказать по этому поводу. Короны у меня точно нет. И себе я не верю в первую очередь. Потом идут монтажники. Потом трассировщики и схемотехники. Дальше разработчики компиляторов и только затем разработчики микросхем. В принципе это пирамида сложности. Проще всего со своими ошибками, хуже всего с ошибками железа микросхем.

                                                                                                    Это мой персональный лист вероятности ошибки (выше по тексту — вероятнее), но и сложности исправления тоже (выше по тексту — проще). Не ошибается только том, кто ничего не делает. Но свои ошибки исправляются проще.

                                                                                                    Ошибки в компиляторах редкость. Но они редкость именно потому, что компиляторы относительно простые. Есть четкая убежденность в том, что с ростом кодовой базы будет расти и вероятность возникновения ошибки. Потому я предпочту жевать привычный кактус в виде C. Я не могу себе позволить сознательно усложнять себе жизнь увеличивая количество предпоследних по уровню сложности устранения ошибок. Да, тут вопрос опята применения, привычек, тараканов в голове — но я смотрю за тем что меня окружает и понимаю — нет, я не один такой. И на мой век такого подхода хватит.

                                                                                                    И хорошо, если я хоть кого-нить смогу воспитать в такой же парадигме тотального контроля и понимания. В принципе у меня это получается. Неоднократно в середине «курсов повышения квалификации начинающих разработчиков» меня спрашивали — чего я на ютубчик не выложу. Я всегда отвечаю — каждый следующий оказывается более хорошим. Я вижу что именно становится непонятным и пытаюсь объяснить эти моменты проще. Не лишайте меня возможности развиваться. И приятно когда люди к тебе возвращаются и говорят — вот ты рассказывал вроде и азы, а когда коснулось именно твой рассказ физику процессов понять помог. Или ушедшие в прикладники заходят в гости и рассказываю как же я был прав, когда говорил что нельзя просто так взять и использовать сторонние библиотеки. А время на подробное их изучение, как правило, не меньше чем время на их реализацию собственными силами. Но в последнем случае ты четко знаешь себе как именно оно работает. Так что пусть не везде и не всегда, но такой подход все равно востребован.
                                                                                                      +1
                                                                                                      А вот как реально внутри устроен контроллер (какие схемотехнические решения скрываются за сокращениям ALU, VIC/NVIC и иже с ними) я себе вполне представляю.

                                                                                                      Представляете или знаете всю принципиальную схему?


                                                                                                      А просто у нас очень разные представления о «есть память». На вскидку мне тяжело даже от VMT (она же vtable).

                                                                                                      А она в плюсах, мягко скажем, не обязательна, вас ведь не заставляют писать виртуальные функции? И полиморфизм времени компиляции (темплейты всякие), кстати, очень помогают этого избежать.


                                                                                                      А у меня всего ведро. Хорошо когда литров на 18, а не на 3.

                                                                                                      Вы всё время так говорите, будто на больших ПК всем сплошь плевать на время выполнения и лишние байты. А лишние байты — это иногда, например, вопрос помещения в кешлайн. И 8 байт (одна восьмая!) на лишний указатель — это может быть очень много, и может уменьшить производительность в два раза, например.


                                                                                                      И хорошо, если я хоть кого-нить смогу воспитать в такой же парадигме тотального контроля и понимания.

                                                                                                      Тотального контроля и понимания, гм. Я до сих пор не понимаю, как вы гарантируете, что в ваших программах нет ошибок?

                                                                                                        –2
                                                                                                        Представляете или знаете всю принципиальную схему?


                                                                                                        Ну что вы, в самом деле. Конечно вся схема это большой коммерческий секрет. Поэтому представляю. Довольно подробно, но естественно не абсолютно точно.

                                                                                                        По плюсам — так меня никто не заставляет вообще их использовать. Я и не использую. Но помните Чеховскую присказку «если в начале пьесы на стене висит ружье, то оно должно выстрелить». Где гарантия того, что мой не попадет к тому, кто выстрелит? Зачем мне такие заморочки? Да, это автоматом поднимает планку сопровождения в плане наращивания фич (ибо устранение багов в обоих случаях будет требовать примерно одинаковых трудозатрат), но практически сразу гарантирует что новая фича не поломает существующий функционал. Если кто из коллег сейчас или в будущем решит переписать любой мой код на любом языке — я возражать не буду. Но вся ответственность будет на нем. Больше того я загружен работой так, что даже начальство пытается найти способы меня разгрузить. Но что-то не наблюдаю толпы желающих. И я не уверен, что здесь исключительно финансово-организационные проблемы. Ибо пытались (и пытаются) многие. Но получается у единиц. Увы, но эти единицы как правило у нас не задерживаются. Впрочем, для них это скорее хорошо. Во всяком случае у меня к ним претензий нет.

                                                                                                        … на больших ПК всем сплошь плевать на время выполнения и лишние байты.


                                                                                                        Да боже упаси. Я, как раз наоборот. Говорю что на больших ПК этому слишком мало внимания уделяется. Но интервью Линуса, в котором он говорит о проектировании git и отсутствии в userspace привычных ядерных граблей очень показательно. Он, если я правильно помню, сказал что отдыхал.

                                                                                                        И да, кеши у нас тоже редкие гости. Да и их размеры на ПК как правило больше размером чем вся наша память. Честь Вам и хвала если вы этими вопросами озадачиватесь.

                                                                                                        Я до сих пор не понимаю, как вы гарантируете, что в ваших программах нет ошибок?


                                                                                                        Во-первых программ без ошибок не бывает. И я не идеален. Наверняка они есть. На моей памяти было два случая когда программа несколько лет верой и правдой работавшая вдруг обнаружила крайне досадный баг. Спасибо прикладникам — быстро устранил.

                                                                                                        Но буквально пара случаев — это исключительно по причине серьезного выходного контроля изделий. Они проверяются по всем статьям — и механника, и климатика, и воздействия ЕМС, и мусор или отвал по входам и все вместе взятое. В отдельных случаях мне позволено перезапускаться с восстановлением работоспособности. Критерии последней четко прописаны. По большому счету именно четкое тестирование изделий и гарантирует в достаточной степени надежность моих программ. И это не авиация, не автомобилестроение, и даже не станкостроение… Что должно твориться там я не знаю. Думаю то же, что у нас но более жестко. Во всяком случае я на это надеюсь.
                                                                                          0
                                                                                          Вы ему скажете. У вас же где-то, пусть даже на бумаге, эта спека есть?
                                                                                          Предположим, у вас есть эта спека, безошибочно составленная системным архитекторам на основе ТЗ, правильно написанного менеджером, общавшимся с клиентом, который знал что ему нужно. Допустим, спеку эту вы досконально прочли, правильно поняли и безупречно реализовали. А спека вообще ссылается на отраслевой стандарт восемьдесят-лохматого года. Казалось бы, что может пойти не так? А потом оказывается, что датчик вообще не тот (тру стори), или что он не соответствует стандарту (тру стори №2) и производитель, разумеется, не собирается это исправлять. Итого система не работает в полном соответствии со спекой. Хоть заверифицируйся, натур испытания никто не отменял.
                                                                                            0
                                                                                            Хоть заверифицируйся, натур испытания никто не отменял.


                                                                                            Уже хотел начать возмущаться, но дочитал до конца и полностью поддерживаю. Разве что уточню что объем натурных испытаний тоже тот еще вопрос. В зависимости от критичности изделия это может быть и выборочное тестирование из партии, и приемо-сдаточные, и периодические. При чем у изделия критика может проверяться приемо-сдаточными и периодикой, а менее критичные моменты выборочным тестированием.

                                                                                            Впрочем, на технических ресурсах, эти чисто менеджерские (в крайне хорошем смысле этого слова) заморочки не в чести. А зря.
                                                                                              0

                                                                                              А если натурные испытания проводятся по стандартам восемьдесят-лохматого года? Хоть обыспытайся, всё тлен.

                                                                                  0
                                                                                  Что-то я сомневаюсь, что в данном случае погромисты были настолько заняты распихиванием переменных по регистрам, что не приметили слона :)
                                                              +2

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


                                                              Искусство — оно не в том, чтобы помнить все UB на зубок.

                                                                –1
                                                                Ох, как философией запахло…

                                                                Нет, общий вывод неоспорим. Конечно все именно так.

                                                                Однако любая струна рано или поздно лопнет. Работа под постоянной нагрузкой даром не проходит. А токсичные свинцовые белила одно время были весьма популярны в качестве косметического средства. Да и в состав красок они входили.

                                                                К чему я? Да к тому, что каждый технологический или научный виток отправляет на свалку истории то, что было сделано раньше. Беда в том, что супер безопасность Rust'а не защитит от уязвимостей класса той же spectre. Вот так раз — и главный козырь оказался побит.

                                                                Однако я не буду делать выводов. Еще раз — в целом я полностью согласен. Искусство — это писать грамотно и безопасно в современном понимании этих понятий. И, конечно, не от одних только UB это зависит.
                                                                  +4
                                                                  Беда в том, что супер безопасность Rust'а не защитит от уязвимостей класса той же spectre.

                                                                  А ещё не защитит от уязвимостей класса «прямой доступ к железу». Раст и не должен защищать от таких уязвимостей.

                                                    +1

                                                    После ваших слов захотелось написать парсер chm на идрисе.


                                                    Правда, потребность не то чтобы реальна.

                                                0

                                                Там немного спорная история ИМХО. Автор пытался обернуть АПИ, завязанное на хендлах с временем жизни, определяемым в рантайме. Т.е. любой хендл мог сдохнуть в любой момент. Такое почти нереально натянуть на глобус лайфтаймов.

                                                  0

                                                  Ну а как си код справляется с этим? Приходит событие что указатель сдох? Вообщем может автору было важнее делать композитор а не выдумывать новые типы данных в раст.

                                                    +1

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


                                                    Если не пытаться слепо следовать мантре «время жизни объекта в Rust» = «время жизни Wayland-объекта», то по идее отлично можно всё разрулить, переводя Rust-объект в неактивное состояние и транслируя событие в Rust-код, чтобы он перестал пользоваться этим объектом.

                                                0
                                                Как не переписать проект на Rust

                                                Все просто, не нужно начинать его изучать. И желаний не появится.
                                                  +2
                                                  Мне надо было вытащить информацию из существующих CHM-файлов, а времени разбираться в формате не было. Лень — двигатель прогресса.

                                                  Если было лень почему не распаковали штатными методами
                                                  hh.exe -decompile output_dir source.chm

                                                  И потом любым перлом парсите и преобразуете обычные html файлы в то что вам надо.
                                                    +2
                                                    библиотеку такого объема не логичнее ли сразу на раст переписать? ~3к строк сишного кода должны транслироваться в <1k строк раста. А точек отказа в итоге будет меньше.
                                                      0

                                                      500 строк обёртки < 1000 строк родной реализации < 3000 оригинальной реализации.


                                                      В общем случае, как мне кажется, лучше будет сначала написать обёртку, обкатать на ней интерфейс, а потом его постепенно заменять на родную реализацию, постоянно сравнивая поведение.

                                                        +1
                                                        500 строк обёртки < 1000 строк родной реализации < 3000 оригинальной реализации.
                                                        но эти 500 строк обертки сложнее чем 1000 строк родной реализации )
                                                        В общем случае, как мне кажется, лучше будет сначала написать обёртку, обкатать на ней интерфейс, ...
                                                        интерфейс новой либы может быть удобнее интерфейса обертки т.к. он не зависит от интерфейса исходной либы
                                                        … а потом его постепенно заменять на родную реализацию, постоянно сравнивая поведение.
                                                        или написать несколько юнит-тестов и лабать пока не сойдется. Тем более что юнит-тесты всё равно нужны
                                                          +1
                                                          500 строк обертки (еще не включая домашнее задание) выглядят неоправданно дорого. По сравнению с околонулевой аналогичной нагрузкой в C++/D/Crystal

                                                          Но вот, что есть генератор биндингов, это очень большой плюс — уж очень FFI бывает нетривиально.
                                                        0
                                                        Может быть я чего-то не понимаю, но действие в самом начале

                                                        $ cat Cargo.toml
                                                        [workspace]
                                                        members = ["chmlib", "chmlib-sys"]


                                                        не должно сработать, команда `cargo new --lib` ведь сама не пропишет members в главный Cargo.toml. Наверное имелось в виду

                                                        $ cat > Cargo.toml
                                                        [workspace]
                                                        members = ["chmlib", "chmlib-sys"]


                                                        чтобы самому руками создать требуемую структуру в Cargo.toml.

                                                        Хотя в оригинале написано так же, как и в переводе. Нет, наверное я всё же что-то не понимаю.
                                                          0

                                                          Действительно, Cargo сам workspace не пропишет.


                                                          Это не то, чтобы исчерпывающий список действий. Просто иллюстрация. Подразумевается, что корневой Cargo.toml будет создан вручную, а вызов cat только демонстрирует его содержимое.

                                                            0
                                                            Смутило как раз то, что нигде не указано что содержимое Cargo.toml надо указывать самому, только `touch Cargo.toml` для его создания, после куча других действий и после в конце `cat Cargo.toml` с выводом содержимого как будто бы оно само появилось в результате предыдущих действий. Но это само собой камень в огород Майкла Брайана, ни в коем случае не вас как переводчика.

                                                        Only users with full accounts can post comments. Log in, please.