Как только вы переступаете через болевой порог Борроу-Чекера и осознаёте, что 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
¦   L-- mozilla_helper.sh
+-- COPYING
+-- Makefile.am
+-- NEWS
+-- NOTES
+-- README
L-- 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
    L-- 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
¦   ¦   L-- RealWorld
¦   ¦       +-- BrowserBack.bmp
...
+-- script
¦   +-- _community
¦   ¦   L-- disqus.js
¦   +-- hs-common.js
...
L-- 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.

Комментарии (43)


  1. Fedcomp
    10.11.2019 08:07

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

    Ну ну
    www.reddit.com/r/rust/comments/biq864/giving_up_on_wlrootsrs


    1. Fedcomp
      10.11.2019 10:33

      Тут еще стоит вспомнить как Tox сервер переписывали на раст и нашли ошибку в C версии.


    1. ilammy Автор
      10.11.2019 12:02
      +1

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


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


      1. Fedcomp
        10.11.2019 13:30
        +2

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

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

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

        Особенно в случае Раста, где количество компетентных желающих на нём писать существенно больше, чем количество желающих за это платить
        В этом в том числе и апологеты C/C++ не помогают кстати.


        1. sshikov
          10.11.2019 14:03
          +1

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

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


          1. Fedcomp
            10.11.2019 14:16

            Тут дело в другом, если переписывать библиотеку какого то ПО на Rust, значит что придется следить за всеми изменениями и вносить правки в свою версию. В случае биндингов — только за актуальностью биндингов. Кроме того в большом софте реально много кода, это просто непрактично переписывать.


            1. MinimumLaw
              10.11.2019 20:39

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

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

              Впрочем, надо посмотреть. Только время все расставит по своим местам. Пока Rust любопытен. Пожалуй, я готов его изучать, но точно не готов писать не нем то, что уйдет в массовое производство. Пока есть сомнения. И в том, что это кто-то кроме меня сможет сопровождать, и в надежности результата в том числе. Да, последнее скорее всего напрасно, но пока оно есть — не о каком программировании для изделия речи не будет. Только «на побаловаться». Ну и скорость… Куда ж без нее. Такты наше все. Чем меньше их съем я, тем больше их останется тем кто выше.


              1. Fedcomp
                11.11.2019 08:36

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

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

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


                1. MinimumLaw
                  11.11.2019 11:59

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


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

                  … по части асинхронных библиотек все очень бурно идет на данный момент (из-за только что стабилизировавшегося 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, на крайний случай ремни и подушки по кругу, вторые кричат что безопасность — это мастерство водителя и возможность тотального контроля поведения машины, который ни одна электронная система никогда не перехватит. По мне спор этот будет длиться еще очень и очень долго. Но рано или поздно победа так или иначе будет за автоматами. Тотальный контроль если и останется, то будет нишевым решением. Так и с языками. Останется С где-нит на самом нижнем уровне. А все, что выше планировщика — все будет на чем-то «безопасном».


                  1. Fedcomp
                    11.11.2019 12:22
                    +2

                    Оставлять 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 поддержка (если вообще присутствовать). Это не значит что не получится скомпилировать под нужную архитектуру, но однозначно придется плясать на каждом шагу.


                    1. MinimumLaw
                      11.11.2019 13:05

                      огромное количество CVE состоящие из банальных buffer overflow довольно красноречиво говорит о том что лучше бы эти дела ловил компилятор.


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

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

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


                      1. Fedcomp
                        11.11.2019 17:49

                        Я вижу в элитарности программирования вред. Машины на автопилоте как и умные компиляторы потенциально лучше человека справляются (и работают быстрее).

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

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

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


                        1. MinimumLaw
                          11.11.2019 21:15
                          -1

                          Стоит ли это понимать, как роспись в собственном бессилии? Ведь компилятор тоже не с небес спустился. Его тоже кто-то написал. И кто-то проверил. Стоит ли так безоговорочно верить ему?

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

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

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

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

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


                          1. 0xd34df00d
                            12.11.2019 02:03

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


                            1. MinimumLaw
                              12.11.2019 07:05

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

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


                              1. 0xd34df00d
                                12.11.2019 07:32

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

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


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


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

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


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

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


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

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


                                1. MinimumLaw
                                  12.11.2019 11:32

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

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


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

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

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

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

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

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

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

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

                                  И еще. Я старался максимально убрать любой сарказм и любые подколки. Если что осталось, прошу простить. Я не со злаю


                                  1. 0xd34df00d
                                    12.11.2019 16:55

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

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


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

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


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

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


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


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

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


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


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


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


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

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


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

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


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


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

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


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


                                    1. Antervis
                                      12.11.2019 17:57

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


                                      1. ilammy Автор
                                        12.11.2019 17:59

                                      1. 0xd34df00d
                                        12.11.2019 18:06

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


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


                                    1. ilammy Автор
                                      12.11.2019 17:57

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

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


                                      1. 0xd34df00d
                                        12.11.2019 18:06

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


                                      1. MinimumLaw
                                        13.11.2019 11:40

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


                                    1. MinimumLaw
                                      13.11.2019 11:25
                                      +1

                                      И функции у вас не инлайнятся...


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

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

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


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

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

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

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

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


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

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

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

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

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


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

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

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

                                      Конечно, все написанное исключительно мое субъективное мнение. Ни коим образом не претендующее на звание «единственно верного и идеологически выдержанного».


                              1. Fedcomp
                                12.11.2019 07:46

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

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

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

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

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

                                Сработает ли этот факт на повышение надежности? Мое мнение — однозначно нет.
                                Но компилятор ловит больше ошибок чем компиляторы C, разве это не повышает надежность?


                                1. MinimumLaw
                                  12.11.2019 11:54

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


                                  А если примеры посложнее, да с safe массивами? А еще замечательный пример — реализация кольцевых буферов, где переполнение чуть ли не основная фишка. Впрочем, я уже ответил выше.


                                1. Antervis
                                  12.11.2019 13:39
                                  +1

                                  Строго говоря с ними разобраться и «выучить» их быстрее чем разбираться со всеми UB в разных компиляторах на разных платформах C.
                                  «undefined behavior» — категория стандарта языка, он не зависит от платформ и компиляторов.


                        1. KanuTaH
                          11.11.2019 23:23

                          Я вижу в элитарности программирования вред. Машины на автопилоте как и умные компиляторы потенциально лучше человека справляются (и работают быстрее).

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

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


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


                          1. MinimumLaw
                            12.11.2019 07:18

                            — Перегрузки больше двадцатикратной могут убить.
                            — Могут. Но падение с высоты пяти километров не может не убить.


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


                      1. 0xd34df00d
                        12.11.2019 02:01

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


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


                        1. MinimumLaw
                          12.11.2019 06:55

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

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

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

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

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


                          1. 0xd34df00d
                            12.11.2019 07:34

                            Беда в том, что супер безопасность Rust'а не защитит от уязвимостей класса той же spectre.

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


          1. 0xd34df00d
            12.11.2019 00:48

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


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


    1. TargetSan
      10.11.2019 12:46

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


      1. technic93
        11.11.2019 22:11

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


        1. ilammy Автор
          11.11.2019 23:13

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


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


  1. 0ri0n
    10.11.2019 09:58

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

    Все просто, не нужно начинать его изучать. И желаний не появится.


  1. kovserg
    10.11.2019 23:34
    +1

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

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

    И потом любым перлом парсите и преобразуете обычные html файлы в то что вам надо.


  1. Antervis
    11.11.2019 01:23
    +1

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


    1. ilammy Автор
      11.11.2019 11:06
      +1

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


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


      1. Antervis
        11.11.2019 17:24

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


      1. Siemargl
        11.11.2019 22:42

        500 строк обертки (еще не включая домашнее задание) выглядят неоправданно дорого. По сравнению с околонулевой аналогичной нагрузкой в C++/D/Crystal

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