Harbour для начинающих

Александр Кресин
2003 - 2016

3. Отличия от Клиппера - краткое описание

3.1 Обзор

Харбор создавался как полностью совместимый с Клиппером компилятор. Это значит, что любая ваша программа, написанная на Клиппере, должна без проблем компилироваться Харбором и работать так же, как если бы она была откомпилирована Клиппером. Во многих случаях так оно и есть, но, конечно, могут возникать и проблемы - не все получается так, как задумано, Харбор не свободен от ошибок и недоделок.
Еще один возможный источник проблем при переходе с Клиппера на Харбор - это дополнительные библиотеки, вставки на C и ассемблере, если вы их используете - 16-разрядные объектники не прилинкуются к 32-разрядному приложению. Если у вас есть исходники этих модулей, то можно попробовать перекомпилировать их, хотя и тут успех не гарантирован - не все функции 16-разрядного C компилятора доступны в 32-разрядном.

Но, конечно же, Харбор не просто повторяет функциональность Клиппера, а существенно расширяет ее - иначе какой был бы в этом смысл ?! Помимо расширений, связанных с переходом от 16 к 32 разрядному коду ( снятие ограничений по памяти, по размерности массивов и длине строк ), Харбор предлагает и языковые расширения, и дополнительные функциональные возможности. Они и будут рассмотрены ниже.
Сразу хочу предупредить, что этот перечень будет неполным - о чем-то я могу забыть, что-то не написать просто из-за нехватки времени и терпения. Начну с немногого, потом постепенно, по мере возможности, буду пополнять этот список и расширять описания. Рекомендую вам также воспользоваться документацией с официального сайта Harbour, a также с постоянно пополняющегося сборника на docs.google.com. Она, правда, тоже не полная, и, к тому же, не на русском - но, даже если у вас с английским проблемы, что-нибудь полезное вы оттуда все равно почерпнете - хотя бы названия новых функций, образцы синтаксиса, и т.д. Очень рекомендую также уже упоминавшийся здесь англоязычный документ xhb-diff.txt, который есть любом дистрибутиве Harbour в каталоге Doc. Здесь содержится описание наиболее важных языковых расширений Harbour ( по сравнению с Clipper ) и отличие их реализаций от xHarbour.

И еще пара слов о языковых новшествах. Лично я использую их только при самой крайней необходимости. Не забывайте, что чем дальше вы от традиционного Clipper-кода, тем больше проблем у вас возникнет при необходимости перейти на какой-либо другой компилятор - кто знает, что в этой жизни может понадобиться. Даже при переходе Harbour <--> xHarbour могут появиться проблемы, т.к. многие новшества по-разному там реализованы - см. xhb-diff.txt. И если переход на другой компилятор может выглядеть весьма отдаленной перспективой, то потребность просмотреть/переконвертировать/модифицировать ваш dbf сторонними средствами может появиться в любой момент - именно поэтому я никогда не использую новые типы полей в dbf - файлах, тем более, что всю необходимую функциональность можно реализовать без них.

3.2 Некоторые языковые расширения

Здесь мы рассмотрим несколько новых операторов, появившихся в Harbour. Начнем с FOR EACH:

      FOR EACH var1 [,var255] IN expr1 [,expr255] [DESCEND]
         [EXIT]
         [LOOP]
         ...
      NEXT
   
- expr - может быть обычным массивом, хэш-массивом или текстовой строкой;
- в переменной var<n> ( var1, ... ) ( enumerator - так мы дальше будем ее называть ) хранится ссылка на элемент массива или строки (если строка передана по ссылке - см.пример ниже ) expr<n>, таким образом, при присвоении ей какого-либо значения, мы изменяем соответствующий элемент массива или строку;
- после завершения цикла enumerator имеет то же значение, что и до цикла;
      Local c := "alles", s := "abcdefghijk"
      FOR EACH c IN @s
         IF c $ "aei"
            c := UPPER( c )
         ENDIF
      NEXT
      ? s      // AbcdEfghIjk
      ? s      // alles
   
- enumerator имеет свойства, к которым можно обращаться, используя ООП синтаксис:
   :__enumindex - индекс, номер текущего элемента в массиве/строке;
   :__enumbase - соответствующий expr<n> ;
   :__enumvalue - значение переменной;
   :__enumkey - ключ элемента хэш-массива.

Еще один новый оператор - WITH OBJECT:

      WITH OBJECT expression
         ...
      ENDWITH
   
Она позволяет при разумном использовании упростить код:
      // можно написать
      WITH OBJECT myobj:a[1]:myitem
         :message( 1 )
         :value := 9
      ENDWITH

      // вместо
      myobj:a[1]:myitem:message( 1 )
      myobj:a[1]:myitem:value := 9
   

Ну и, наконец, SWITCH:

      SWITCH expr
        CASE value
          [EXIT] 
        ... 
        OTHERWISE 
      ENDSWITCH
   
Это выглядит примерно так:
      SWITCH a
      CASE 1
         ? "FOUND: 1"
      CASE "2"
         ? "FOUND: 2"
      OTHERWISE
         ? "other"
      END
   
Это C-подобный оператор, он похож на Клипперовские DO CASE ... ENDCASE и IF ... ELSEIF ... ELSE ... ENDIF, но не позволяет в качестве условия использовать выражения, зато благодаря особенностям реализации работает гораздо быстрее, так что его имеет смысл использовать в циклах.

В Harbour существенно расширены возможности использования макросов. Так, теперь в макросах ( и в кодоблоках ) можно использовать операторы ++, --, +=, -= и т.п., вложенные кодоблоки, выражения произвольной длины. Также допустимо использовать списки:

      cMacro := "1, 150, 'Alexey Ivanov', 20, .T."
      { &cMacro } // массив
      SomeFun( &cMacro ) // Список аргументов

      cMacro := "1,10"
      SomeArray[ &cMacro ] // индексы массива
   
Макросы можно использовать для имен переменных объектов:
      Procedure main()
         memvar var
         local o := errorNew(), msg := "cargo"
         private var := "CAR"

         o:&msg := ""
         o:&( upper( msg ) ) += ""
         ? o:&var.go
   

В Harbour реализованы расширенные кодоблоки. Такой кодоблок представляет собой многострочный фрагмент текста, в котором допустимы любые языковые элементы, в том числе Local и Static переменные и является, по сути, функцией, вложенной в функцию:

      Function Main

         Local b1, b2

         b1 := Fnc1( Seconds() )
         ? Eval( b1 )

         ? "Press any key"
         inkey(0)

         b2 := Fnc1( Seconds() )
         ? Eval( b2 )

         ? "Press any key"
         inkey(0)

         ? Eval( b1,Seconds() )

         ? "Press any key"
         inkey(0)

         ? Eval( b1 )
         ?

      Return Nil

      Function Fnc1( nSec )

         Local bCode := {|p1|
            Local tmp := nSec
            IF p1 != Nil
               nSec := p1
            ENDIF
            Return tmp
         }

      Return bCode
   

Этот пример не так прост, как кажется на первый взгляд, он демонстрирует интересную особенность реализации кодоблоков, которая существовала и в Клиппере, но в полной мере смогла раскрыться в этих расширенных кодоблоках. Обратите внимание на параметр nSec, который является локальной переменной функции Fnc1. Он внешний по отношению к кодоблоку bCode, но и после завершения работы Fnc1 продолжает "жить" вместе с bCode. На первый взгляд может показаться, что такая переменная - своеобразный аналог Static, но это не так - дело в том, что у каждого экземпляра кодоблока будет свой экземпляр этой переменной, инициализированный в процессе вызова и работы функции Fnc1. Это называется "замыкание" (closure) - см. статью в Википедии, элемент, хорошо известный по некоторым современным языкам программирования. В Javascript, например, он используется очень широко и считается одним из наиболее интересных и мощных его инструментов.

Элементы массива, хэш-массива и объекты в Harbour можно передавать по ссылке:

      function main()
      Local arr := { 1,2,3 } 

         ? arr[1], arr[2], arr[3]   // 1   2   3
         p( @arr[2] )
         ? arr[1], arr[2], arr[3]   // 1  12   3

      return nil

      Function p( x )
         x += 10
      return nil
   

Функции с переменным количеством параметров. Harbour позволяет декларировать в объявлении функции и поименованные параметры, и затем неименованные. Эти неименованные параметры могут затем использоваться при помощи оператора "...":

      // как элементы массива
      Procedure main()
         AEval( F( "1", "2", "A", "B", "C" ), {|x, i| qout( i, x ) } )

      func f( p1, p2, ... )
         ? "P1:", p1
         ? "P2:", p2
         ? "other parameters:", ...
      return { "X", ... , "Y", ... "Z" }

      // как индексы массива:
      Procedure main()
         local a := { { 1, 2 }, { 3, 4 }, 5 }
         ? aget( a, 1, 2 ), aget( a, 2, 1 ), aget( a, 3 )

      func aget( aVal, ... )
      return aVal[ ... ]

      // как параметры функции:
      Procedure main()
         info( "test1" )
         info( "test2", 10, date(), .t. )

      Procedure info( msg, ... )
         qout( "[" + msg +"]: ", ... )
   
Функция hb_aParams() возвращает список полученных параметров, и поименованных, и непоименованных.
Функция hb_arrayToParams() конвертирует массив в список элементов, который затем могут использоваться как параметры функции, индексы массива и пр.:
      // hb_arrayToParams(  ) -> [ 1 ] [, [ N ] ]
      // Например:
      Procedure main( ... )
         local aParams := hb_aParams(), n
         /* убираем параметры, начинающиеся с '--' */
         n := 1
         while n < len( aParams )
            if left( aParams[ n ], 2 ) == "--"
               hb_adel( aParams, n, .t. )
            else
               ++n
            endif
         enddo
         ? "Public parameters:", hb_arrayToParams( aParams )
      return
   

Новый тип данных - TIMESTAMP, он определен и в RDD (т.е. может использоваться как тип поля в dbf), и в VM (виртуальной машине). Для переменной типа TIMESTAMP функция Valtype вернет "T", с ней можно производить операции сложения, вычитания и сравнения. Константы этого типа данных можно декларировать в программе следующим образом:

      // { ^ [ YYYY-MM-DD [,] ] [ HH[:MM[:SS][.FFF]] [AM|PM] ] }
      // t"YYYY-MM-DD [H[H][:M[M][:S[S][.f[f[f[f]]]]]]] [PM|AM]"
      // t"YYYY-MM-DDT[H[H][:M[M][:S[S][.f[f[f[f]]]]]]] [PM|AM]"
      Procedure main()
         Local t1 := {^ 2012-11-28 10:22:30 }   // или {^ 2012/11/28 10:22:30 }
         Local t2 := t"2009-03-21 17:31:45.437" // или t"2009-03-21T17:31:45.437"

         ? t1 + 1      // 11/29/12 10:22:30.000
         ? t1 - t2     // 1347.701905
   
Кстати, раз речь зашла об объявлении констант, в Harbour можно объявлять константы типа ДАТА вот так:
      Local d1 := 0d20121201  // Шаблон: 0dYYYYMMDD
   
А строковые константы можно объявлять, как в C, используя литерал e"...":
      Local str1 := e"Helow\r\nWorld \x21\041\x21\000abcdefgh"
   
Целые числа можно указывать в шестнадцатиричном формате:
      Local n := 0x1F
   

Еще одно интересное новшество в Harbour - указатели на функции. Их можно создавать на этапе сборки приложения, используя выражение @<funcName>(), или динамически во время исполнения с помощью макроподстановки: &("@<funcName>()"). Valtype() возвращает для таких указателей S.

      Procedure main()
         local funcSym
         funcSym := @str()
         ? funcSym:name, "=>", funcSym:exec( 123.456, 10, 5 )
         funcSym := &( "@upper()" )
         ? funcSym:name, "=>", funcSym:exec( "Harbour" )
      return
   

3.3 Классы и объекты

Реализация ООП ( объектно-ориентированного программирования ) - это, пожалуй, первое, что добавили разработчики Harbour к стандарту языка. Клиппер последних версий содержал элементы ООП. Там было несколько предопределенных классов, можно было создавать их объекты, но свои классы создавать было нельзя. Были, правда некоторые недокументированные возможности, которые позволяли это делать, на них были основаны несколько 3rd party библиотек, реализующих ООП для Клиппера, наиболее известные из них - это Fivewin (точнее, Objects.lib из комплекта Fivewin) и Class(y). В Harbour для использования классов не требуется подключать дополнительные библиотеки, ООП является здесь частью языка и, поскольку соответствующие средства составляют часть ядра, Harbour предоставляет большие возможности, чем Клиппер со всеми сторонними библиотеками.

Итак, чтобы создать новый класс, используется команда CLASS ... ENDCLASS:

      [CREATE] CLASS <cClassName> [ FROM | INHERIT <cSuperClass1> [, ... ,<cSuperClassN>] ]
                [ MODULE FRIENDLY ] [ STATIC ] [ FUNCTION <cFuncName> ]

      [HIDDEN:]
         [ CLASSDATA | CLASSVAR  | CLASS VAR <DataName1>]
         [ DATA | VAR  <DataName1> [,<DataNameN>] [ AS <type> ] [ INIT <uValue> ]
                [[EXPORTED | VISIBLE] | [PROTECTED] | [HIDDEN]] [READONLY | RO] ]
         ...
         [ METHOD <MethodName>( [<params,...>] ) [CONSTRUCTOR] ]
         [ METHOD <MethodName>( [<params,...>] ) INLINE <Code,...> ]
         [ METHOD <MethodName>( [<params,...>] ) BLOCK  <CodeBlock> ]
         [ METHOD <MethodName>( [<params,...>] ) EXTERN <funcName>([<args,...>]) ]
         [ METHOD <MethodName>( [<params,...>] ) SETGET ]
         [ METHOD <MethodName>( [<params,...>] ) VIRTUAL ]
         [ METHOD <MethodName>( [<params,...>] ) OPERATOR <op> ]
         [ ERROR HANDLER <MethodName>( [<params,...>] ) ]
         [ ON ERROR <MethodName>( [<params,...>] ) ]
         ...
      [PROTECTED:]
         ...
      [VISIBLE:]
      [EXPORTED:]
         ...

      [FRIEND CLASS <ClassName,...>]
      [FRIEND FUNCTION <FuncName,...>]

      [SYNC METHOD <cSyncMethod>]

      ENDCLASS [ LOCK | LOCKED ]
   

Класс может быть объявлен как наследник от одного или нескольких родителей (особенности multiinheritance - множественного наследования мы рассмотрим позже) при помощи служебного слова FROM или INHERIT. Это значит, что он получает от родительского класса весь его набор переменных и методов (кроме помеченных как HIDDEN). Любой из унаследованных методов может быть переопределен. При этом родительский метод может быть вызван как Super:<methodName>() или ::<cSuperClass>:<methodName>() (если он не HIDDEN).

Отмечу еще одну интересную особенность, связанную с наследованием. Если в порожденном классе есть переменная с тем же именем, что и в родительском, то каждый объект содержит две переменных с этим именем. К одной из них следует обращаться obj:<varName>, к другой - obj:<cSuperClass>:<varName>.

Класс может быть объявлен как STATIC - т.е. он не может использоваться вне текущего модуля.

Класс может быть объявлен как MODULE FRIENDLY - это значит, что все классы и функции, объявленные в текущем модуле, его друзья (friends), т.е. имеют доступ к его HIDDEN и PROTECTED переменным и методам. И раз уж мы заговорили о дружественных функциях, то обратите внимание на строчки FRIEND CLASS и FRIEND FUNCTION - они определяют дружественные для этого класса классы и функции.

Слова FUNCTION <cFuncName> в объявлении класса определяют его как скалярный класс.

Предложения DATA или VAR - это определения переменных класса. При этом может быть строго определен тип переменной AS <type> (Character, Numeric, Date, Logical, Codeblock, Nil), задано ее начальное значение по умолчанию INIT <uValue> и определен Scope (Hidden,Protected,Exported,Readonly). Scope может быть задан как для каждой переменной отдельно ( в объявлении DATA ), так и для группы переменных и методов:

    - HIDDEN - переменная или метод доступны только внутри класса, где они определены и не наследуются;
    - PROTECTED - доступны только для класса, где они определены и для наследников;
    - EXPORTED ( или VISIBLE ) - доступны отовсюду;
    - READONLY ( или RO ) - если переменная определена как EXPORTED, то модифицировать ее значение можно только из родного класса и его наследников, если как PROTECTED, то только из родного.

Переменные могут принадлежать объекту, а могут и всему классу - это переменные класса, они объявляются как CLASSDATA, CLASSVAR или CLASS VAR. Такая переменная доступна из любого объекта класса как obj:<DataName>, но хранится она не в областях данных объектов, а в одном месте, в области данных класса, и поэтому, естественно, имеет одно значение для всех объектов класса. Есть одно отличие в реализации CLASSDATA с одной стороны, и CLASSVAR или CLASS VAR с другой. Эти переменные могут быть объявлены как SHARED. Это означает, что если в родительском классе есть такая переменная, то производные классы используют ту же самую единственную ее копию, разделяют ее. Если же SHARED не объявлен, то производные классы наследуют ее обычным образом, создавая новую копию переменной для каждого класса-наследника. Так вот, для CLASSVAR и CLASS VAR это работает, как было описано выше, а CLASSDATA переменные ведут себя как SHARED независимо от того, используется это слово в объявлении или нет. Это было сделано для обратной совместимости с Fivewin программами для Клиппера.

Предложения METHOD - это определения методов класса. В общем случае, если метод не INLINE, не BLOCK, и не VIRTUAL,после определения класса (после ENDCLASS) необходимо поместить реализацию метода:

      METHOD ( [] ) CLASS 
         ...
      RETURN Self
   
Метод должен возвращать ссылку на объект ( Self ), если он используется как конструктор.

Ниже описаны разные способы объявления метода:

    - INLINE позволяет поместить реализацию метода в той же строке, где он объявлен. Такой способ может применяться, когда соответствующий код достаточно невелик, это делает объявление метода более простым и наглядным.
    - BLOCK похож на INLINE, но он предполагает запись реализации метода в виде кодоблока и позволяет передавать произвольное количество параметров.
    - EXTERN <funcName>([<args,...>]) применяется, если внешняя функция <funcName>() реализует то, что нужно этому методу.
    - SETGET используется для вычисляемых данных, он позволяет использовать имя метода как имя переменной для чтения / записи данных. Допустим, у вашего класса есть переменная, значения которой нежелательно устанавливать напрямую, поскольку она должна иметь какой-то определенный формат или может находиться только в каком-то диапазоне. Вы определяете эту переменную как HIDDEN, чтобы исключить непосредственное ее изменение, и определяете SETGET метод, который будет использоваться для установки значения переменной - он проверяет передаваемое значение, форматирует его так, как надо и т.д. Теперь, чтобы установить значение переменной, вы пишете: obj:<methodName> := <value> .
    - VIRTUAL метод не делает ничего. Его можно применять для базовых, родительских классов - в этом случае конкретные реализации метода будут сделаны в классах - наследниках.
    - OPERATOR <op> используется для перегрузки оператора <op> ( "+", "-", ... ) - т.е. когда в программе над объектом совершается действие, обозначаемое <op>, это действие реализуется методом, который указан со словом OPERATOR. В качестве параметра методу передается второй объект, участвующий в операции <op>.

Еще один способ объявления метода - задать его с помощью ERROR HANDLER или ON ERROR. Этот метод вызывается в том случае, если объекту класса, к которому он принадлежит, послано сообщение, не определенное для этого класса; иными словами, вызван метод или переменная, которой нет в этом классе. Можно, конечно, использовать такой метод по прямому назначению - для обработки ошибок, но есть и более интересные способы применения. Так, с его помощью можно динамически имитировать наличие методов и переменных объекта, которые не были определены при объявлении класса, причем у разных объектов одного и того же класса могут быть "сымитированы" разные методы и переменные.

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

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

Для создания и проверки работы простейшего класса достаточно, например, написать:

      #include "hbclass.ch"
      FUNCTION Main
      Local obj := myFirstClass():New(3)

         ? obj:x       // 3
         ?
      RETURN Nil

      CLASS myFirstClass
         VAR   x
         METHOD New( n )   INLINE ( ::x := n, Self )
      ENDCLASS
   

Я воспользовался здесь объявлением INLINE (встроенного) метода, чтобы обойтись только конструкцией CLASS ... ENDCLASS и не описывать метод New(n) отдельно. Здесь следует обратить внимание на следующие моменты:

    - в prg, содержащий объявления классов, необходимо включить hbclass.ch;
    - переменная объекта внутри метода этого объекта предваряется двойным двоеточием ::x;
    - сам объект внутри своего метода именуется Self;
    - метод, используемый для создания нового объекта, должен возвращать ссылку на этот объект: Self;
    - как создать новый объект: <имя_класса>():<метод>()    ( obj := myFirstClass():New(3) ).

Теперь пример чуть посложнее:

      #include "hbclass.ch"
      FUNCTION Main
         Local obj1, obj2

         obj1 := mySecondClass():New( 10,"Alex" )
         ? "Всего объектов mySecondClass:", mySecondClass():nKolObj
         obj2 := mySecondClass():New( 11,"Mike" )
         ? "Всего объектов mySecondClass:", mySecondClass():nKolObj
         ? obj1:x, obj2:x
         ? 
      RETURN Nil

      CLASS myFirstClass
         VAR   x
         METHOD New( n )   INLINE ( ::x := n, Self )
      ENDCLASS

      CLASS mySecondClass  FROM myFirstClass
      HIDDEN:
         VAR   cStringS

      EXPORTED:
         CLASS VAR  nKolObj     INIT 0
         VAR        cString1    INIT "Sample"
         METHOD New( n, s )
      ENDCLASS

      METHOD New( n, s ) CLASS mySecondClass

         Super:New( n )
         ::nKolObj ++
         ::cStringS := s
      RETURN Self
   
Здесь появились вот такие новые моменты:
    - у нас уже два класса, myFirstClass и mySecondClass, причем mySecondClass является наледником myFirstClass;
    - двум переменным, nKolObj и cString1, в объявлении класса присвоены начальные значения, они будут по умолчанию у любого нового объекта класса;
    - метод New класса mySecondClass описан отдельно при помощи METHOD <methodName> CLASS <className>;
    - в этом методе используется Super:New(n) для вызова одноименного метода из родительского класса для инициализации переменных объекта, унаследованных от родителя;
    - в mySecondClass переменная cStringS имеет атрибут (scope) HIDDEN (т.е. она доступна только из методов этого класса), а остальные переменные и метод New отмечены как EXPORTED (т.е. доступные отовсюду);
    - у нас в mySecondClass появилась CLASS VAR nKolObj - переменная, принадлежащая не объекту, а всему классу, здесь она используется как счетчик объектов этого класса;

Еще один пример, демонстрирующий объявления методов как BLOCK и EXTERN:

      #include "hbclass.ch"
      function Main()
      Local o1 := HSamp1():New()
      Local o2 := HSamp2():New()

         ? o1:x, o2:x                        // 10       20
         ? o1:Calc( 2,3 ), o2:Mul( 2,3 )     // 60      120
         ? o1:Sum( 5 )                       // 15
         ?

      return nil

      CLASS HSamp1

         DATA x   INIT 10
         METHOD New()  INLINE Self
         METHOD Sum( y )  BLOCK {|Self,y|::x += y}
         METHOD Calc( y,z ) EXTERN fr1( y,z )
      ENDCLASS

      CLASS HSamp2

         DATA x   INIT 20
         METHOD New()  INLINE Self
         METHOD Mul( y,z ) EXTERN fr1( y,z )
      ENDCLASS

      Function fr1( y, z )
      Local o := QSelf()
      Return o:x * y * z
   
Обратите внимание на вызов функции QSelf(), она вовращает ссылку на объект, в контексте которого она находится, т.е., то, что в методах класса называется Self.

А вот пример использования ERROR HANDLER:

      #include "hbclass.ch"
      FUNCTION Main
         LOCAL oTable

         oTable := Table():Create( "sample.dbf", { {"F1","C",10,0}, {"F2","N",8,0} } )
         oTable:AppendRec()
         oTable:F1 := "FirstRec"
         oTable:F2 := 1
         oTable:AppendRec()
         ? oTable:nRec                // 2
         oTable:nRec := 1
         ? oTable:nRec                // 1
         ? oTable:F1, oTable:F2       // "FirstRec"     1
         ?

         RETURN nil

      CLASS Table

         METHOD Create( cName, aStru )
         METHOD AppendRec()   INLINE dbAppend()
         METHOD nRec( n )     SETGET

         ERROR HANDLER OnError( xParam )
      ENDCLASS

      METHOD Create( cName, aStru ) CLASS Table

         dbCreate( cName, aStru )
         USE (cName) NEW EXCLUSIVE
      RETURN Self

      METHOD nRec( n ) CLASS Table

         IF n != Nil
            dbGoTo( n )
         ENDIF
      RETURN Recno()

      METHOD OnError( xParam ) CLASS Table
      Local cMsg := __GetMessage(), cFieldName, nPos
      Local xValue

         IF Left( cMsg, 1 ) == '_'
            cFieldName := Substr( cMsg,2 )
         ELSE
            cFieldName := cMsg
         ENDIF
         IF ( nPos := FieldPos( cFieldName ) ) == 0
            Alert( cFieldName + " wrong field name!" )
         ELSEIF cFieldName == cMsg
            RETURN FieldGet( nPos )
         ELSE
            FieldPut( nPos, xParam )
         ENDIF

      Return Nil
   
Здесь демонстрируется предельно упрощенный класс Table, каждый объект которого oTable должен соответствовать вновь созданной или открытой ( метод Open здесь опущен ) таблице (dbf файлу). Мы хотим обращаться к полям этой таблицы как к свойствам (переменным) объекта oTable. Но мы не можем ввести эти имена полей в объявление класса, т.к. они, вообще говоря, неизвестны на момент написания класса, и они просто-напросто разные для разных таблиц. Здесь нам поможет ERROR HANDLER. Когда происходит обращение к несуществующей переменной ( или методу ) класса, генерируется ошибка и вызывается метод, определенный как ERROR HANDLER ( или ON ERROR ). Из этого метода при помощи функции __GetMessage() можно получить то самое несуществующее имя переменной, к которой было обращение. Причем, если была попытка записать что-то в эту переменную, то полученное имя предваряется символом "_" и методу передается записываемое значение как первый параметр. Все остальное, думаю, ясно из примера.
    В вышеприведенном фрагменте мы видим также пример использования SETGET метода. Обратите внимание, что обращение к SETGET методу nRec происходит как к переменной объекта. Если мы пытаемся установить ее ( oTable:nRec := 1 ), то методу передается устанавливаемое значение как параметр и он производит перемещение по таблице, если же читаем ( ? oTable:nRec ), то просто возвращает результат Recno().

Далее следует список функций для манипуляции классами и объектами:

lExist := __objHasData( oObject, cName )Возвращает логическое значение, указывающее, есть ли у объекта oObject переменная с именем cName
lExist := __objHasMethod( oObject, cName )Возвращает логическое значение, указывающее, есть ли у объекта oObject метод с именем cName
aNames := __objGetMsgList( oObject, [lData], [nClassType] ) Возвращает массив имен всех переменных или методов объекта oObject; lData определяет, что требуется, методы ( .F. ), или переменные ( .T., значение по умолчанию ); nClassType определяет, какие переменные должны попасть в список. Возможны 3 значения, определенные в hboo.ch:
        HB_MSGLISTALL    0   все переменные
        HB_MSGLISTCLASS  1   переменные класса CLASS DATA
        HB_MSGLISTPURE   2   переменные объекта DATA
        
aNames := __objGetMethodList( oObject ) Возвращает массив имен всех методов объекта oObject
aData := __objGetValueList( oObject, [aExcept] )Возвращает двумерный массив всех переменных объекта oObject ( имя и значение ); aExcept - массив имен переменных, которые не надо включать в результат.
oObject := __ObjSetValueList( oObject, aData )Устанавливает значения переменных объекта oObject, aData - двумерный массив имя / значение
oObject := __objAddMethod( oObject, сMethodName, nFuncPtr )Добавляет метод в уже существующий класс, к которому принадлежит объект oObject; сMethodName - имя метода, nFuncPtr - указатель на функцию, реализующую этот метод ( про указатели на на функции см. в подразделе 3.2:
        oHappy := HBClass():New( "THappy" )
        __objAddMethod( oHappy, "Smile", @MySmile() )
        ? oHappy:Smile( 1 )

        Static Function MySmile( nType )
        Return Iif( nType==0,":)",":):)" )
        
oObject := __objAddInline( oObject, cInlineName, bInline )Добавляет inline метод в уже существующий класс, к которому принадлежит объект oObject; сInlineName - имя метода, bInline - кодоблок, реализующий этот метод.
oObject := __objAddData( oObject, cDataName )Добавляет переменную в уже существующий класс, к которому принадлежит объект oObject; cDataName - имя новой переменной
oObject := __objModMethod( oObject, сMethodName, nFuncPtr )Заменяет реализацию метода в уже существующем классе, к которому принадлежит объект oObject; сMethodName - имя метода, nFuncPtr - указатель на функцию, реализующую этот метод ( см. описание функции __objAddMethod() )
oObject := __objAddInline( oObject, cInlineName, bInline )Заменяет реализацию inline метода в уже существующем классе, к которому принадлежит объект oObject; сInlineName - имя метода, bInline - кодоблок, реализующий этот метод. ( см. описание функции __objAddInline() )
oObject := __objDelMethod( oObject, сMethodName )Удаляет метод, или inline метод с именем сMethodName из класса, к которому принадлежит объект oObject
oObject := __objDelInline( oObject, сMethodName )Удаляет метод, или inline метод с именем сMethodName из класса, к которому принадлежит объект oObject
oObject := __objDelData( oObject, сDataName ) Удаляет переменную с именем сDataName из класса, к которому принадлежит объект oObject
lIsParent := __objDerivedFrom( oObject, xSuper ) Возвращает логическое значение, указывающее, является ли класс xSuper (который может быть задан как объект или имя класса ), родителем для класса, к которому принадлежит объект oObject
oNew := __objClone( oSource ) Клонирует объект.
xResult := __objSendMsg( oObject, cName [,xParams...] )Посылает сообщение объекту oObject: может использоваться для получения значения переменной объекта cName или для присвоения ей нового значения ( в этом случае перед ее именем надо поставить префикс "_", а также для вызова метода объекта.

3.4 Lang API (язык программы)

Эта подсистема предназначена для вывода аварийных сообщений и результата функций CMonth(), CDow() - но, в отличие от аналогичных средств в Клиппере, она позволяет менять язык программы во время исполнения.
Чтобы подключить ее к вашему приложению, надо указать в link - скрипте библиотеку hblang.lib и включить в главный prg файл предложения REQUEST с именами тех языков, которые вы предполагаете использовать, например:

     REQUEST HB_LANG_RU866
     REQUEST HB_LANG_RUWIN
   
Hb_LangSelect( cLangID ) --> cLangIDУстанавливает язык программы, возвращает ID предыдущего языка.
Hb_LangName() --> cLangIDВозвращает название языка программы.
Hb_LangErrMsg( nError ) --> cErrorВозвращает текст ошибки по номеру.
Hb_LangMessage( nMessage ) --> cMessageВозвращает текст аварийного сообщения по номеру.

3.5 Codepage API

Эта подсистема обеспечивает поддержку национальных кодовых страниц для ваших данных. Чтобы подключить ее к вашему приложению, надо указать в link - скрипте библиотеку hbcpage.lib и включить в главный prg файл предложения REQUEST с именами тех кодовых страниц, которые вы предполагаете использовать, например:

     REQUEST HB_CODEPAGE_RU866
     REQUEST HB_CODEPAGE_RUKOI8
     REQUEST HB_CODEPAGE_RU1251
   

Далее, следует определить главную кодовую страницу приложения. Именно эта кодовая страница будет использоваться для функций IsUpper(), IsLower(), IsAlpha(), Upper(), Lower(), Transform() и при сравнении строк. В консольном приложении, по-видимому, следует использовать "RU866", а в Windows GUI приложении - "RU1251".
Для установки главной кодовой страницы используется функция

     hb_cdpSelect( sCodepage )
      например:
     hb_cdpSelect( "RU866" )
   

Подобная возможность есть и в Клиппере ( путем подключения специальных obj ), но в Харборе, кроме того, можно определить кодовую страницу для любого открываемого файла данных - при этом все строки будут автоматически транслироваться в главную кодовую страницу приложения при чтении и обратно - при записи, транслироваться будут и строки в dbSeek(), dbLocate(), dbSetFilter(). Достаточно указать кодовую страницу при открытии файла - и вся последующая работа с ним будет выглядеть так, как будто данные находятся в основной кодовой странице:

      USE file_name ... CODEPAGE "RU1251"
      или
      dbUseArea( .T.,,file_name,,.T.,.F.,"RU1251" ) 
   

По умолчанию файл открывается с главной кодовой страницей приложения.
Эту возможность ( определение кодовой страницы файла ) следует использовать только с родными RDD - DBFNTX и DBFCDX.
ADS RDD предоставляет для этих целей другие средства ( настройка ini файла или SET CHARTYPE TO OEM/ANSI ).

Codepage API включает также функцию для трансляции строк из одной кодовой страницы в другую:

      Hb_Translate( sData, sCodepageIN, sCodepageOUT )
      например:
      Hb_Translate( "Привет", "RU1251", "RU866" )
   

и набор функций для utf8:

hb_StrToUtf8( sData, sCodepageIN ) Трансляция sData из sCodepageIN в utf8
hb_Utf8ToStr( sData, sCodepageOUT ) Трансляция sData из utf8 в sCodepageOUT
  
hb_Utf8Len( sData ) Эта и последующие функции - аналоги
hb_utf8Chr( n ) стандартных строковых функций Len(),
hb_utf8Asc( sData ) Chr(), Asc() и т.д., но для строк в
hb_utf8Substr( sData, n1, n2 ) кодировке utf8. Как известно, в utf8
hb_utf8Left( sData, n1 ) один символ может кодироваться больше
hb_utf8Right( sData, n1 ) чем одним байтом. Эти функции оперируют
hb_utf8Stuff( sData, n1, n2, cNew ) количеством символов, а не байт.
hb_utf8Peek( sData, n1 )  
hb_utf8Poke( sData, n1, n )  

3.6 Работа с hrb - файлами

Я уже отмечал здесь, что Харбор может вместо C файла создать из вашего prg особый тип файла с расширением hrb, который содержит p-code вашей программы и может быть исполнен утилитой Hbrun.exe.
Но этот hrb может быть вызван на исполнение и непосредственно из вашей программы - для этого предусмотрены специальные функции:

hb_hrbRun( hrbName, ... (параметры))Исполняет hrb файл с именем hrbname, передает ему список параметров и возвращает результат.
handle := hb_hrbLoad( [ nOptions, ] hrbName [, xparams,... ] )Загружает в память p-code из hrb файла с именем hrbname, возвращает указатель на эти инструкции; xparams - параметры, которые передаются INIT PROCEDURE этого hrb-модуля;
nOptions - необязательный параметр (если его нет, первым параметром будет hrbname), который может иметь следующие значения (константы определены в hbhrb.ch):
   HB_HRB_BIND_DEFAULT    0x0  /* Не переопределяет никакие функции, игнорирует
                               PUBLIC функцию в загружаемом модуле, если функция
                               с таким именем уже определена */
   HB_HRB_BIND_LOCAL      0x1  /* Не переопределяет никакие функции, но оставляет
                               локальные ссылки, т.е., если модуль содержит
                               функцию FOO и такая функция уже определена, то в
                               HRB она конвертируется в STATIC */
   HB_HRB_BIND_OVERLOAD   0x2  /* Переопределяет уже существующие PUBLIC функции */
   HB_HRB_BIND_FORCELOCAL 0x3  /* Конвертирует все PUBLIC функции в STATIC */
   HB_HRB_BIND_LAZY       0x4  /* Не проверяет ссылки, позволяет загружать HRB
                               с неразрешенными к моменту загрузки ссылками */
         
hb_hrbDo( handle, ... (параметры))Исполняет p-code из hrb файла, предварительно загруженного с помощью hb_hrbLoad(), на которые указывает handle, передает им список параметров и возвращает результат.
hb_hrbUnload( handle )Выгружает p-code, на который указывает handle, из памяти.
  
hb_hrbGetFunsym( handle, functionName )Получает указатель на функцию с именем functionName, определенную в hrb файле; предварительно надо загрузить этот hrb в память с помощью handle := hb_HrbLoad( hrbName ). Выполнить эту функцию можно при помощи Do( hFun, ... (параметры)), где hFun - указатель полученный от hb_hrbGetFunsym.
aFuncs := hb_hrbGetFunList( handle, nType )Возвращает список функций в hrb-модуле, nType определяет тип функций (константы определены в hbhrb.ch):
   HB_HRB_FUNC_PUBLIC     0x1   /* Локально определенные PUBLIC функции */
   HB_HRB_FUNC_STATIC     0x2   /* Локально определенные STATIC функции */
   HB_HRB_FUNC_LOCAL      0x3   /* Локально определенные функции */
   HB_HRB_FUNC_EXTERN     0x4   /* Внешние функции, вызываемые в HRB модуле */
         

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

     REQUEST <имя_функции>
   
Можно включить #include "hbextern.ch" - в нем собраны REQUEST на все Харборовские функции. Размер программы в результате, конечно, увеличится, зато у вас будет гарантия, что все функции включены.

Обратите внимание, что параметр hrbname функций hb_hrbRun() и hb_hrbload() может содержать не только имя hrb файла, но и сам p-code, предварительно загруженный с помощью, например, Memoread(), или полученный в результате компиляции с помощью hb_compileBuf() или hb_compileFromBuf() (см. описание этих функций здесь):

      FUNCTION Main()
      Local han, buf, cBuf := ""

         cBuf += "Procedure MM" +Chr(13)+Chr(10)
         cBuf += "? 'Just a test!'" +Chr(13)+Chr(10)

         buf := hb_compileFromBuf( cBuf, "harbour", "/n" )
         hb_hrbRun( buf )
         ?
      Return Nil
   

И еще один интересный момент. Я уже отмечал, что hrb файлы очень похожи по функциональности на p-code dll. И действительно, функция hb_hrbLoad() загружает p-code в пространство вашего приложения так же, как функция hb_libLoad() подгружает динамическую библиотеку. А значит, функции из hrb файла можно вызывать таким же образом, т.е. напрямую, без всяких hb_hrbGetFunsym() и Do(). Для этого, как и в случае с использованием p-code dll, надо предварительно объявить эти функции в вашем приложении как DYNAMIC:

      DYNAMIC HRBFUNC1
      FUNCTION Main()
      Local x, handle := hb_hrbLoad( "my.hrb" )
      
      x := hrbFunc1()   // hrbFunc1 - функция из my.hrb
      
      hb_hrbUnload( handle )
      Return Nil
   

3.7 Встроенный C

В Харборе есть возможность вставлять фрагменты C кода в prg файл. Это может быть удобно, если вам лень из-за пары нужных C функций создавать .с файл и вставлять его в проект. Делается это при помощи директив #pragma BEGINDUMP ... #pragma ENDDUMP.

   #pragma BEGINDUMP
      #include < extend.h >
      #include < math.h >
      HB_FUNC( SIN )
      {
         double x = hb_parnd(1);
         hb_retnd( sin( x ) );
      }
   #pragma ENDDUMP
   

3.8 Хэш массивы

Хэш массивы ( или Хэш-таблицы - см. статью в Википедии - это одна из реализаций структуры данных, известной под названием ассоциативные массивы, которая хранит пары ( ключ,значение ) и позволяет выполнять, как минимум, три операции: добавление новой пары, поиск по ключу и удаление по ключу. Поддержка ассоциативных массивов есть во многих интерпретируемых языках программирования высокого уровня, например, в Perl, PHP, Python, Ruby, и др. Теперь она реализована и в Harbour.

Создается новый хэш-массив функцией hb_hash(), ее можно использовать без параметров ( в этом случае инициализируется пустой хэш-массив ) и с параметрами, определяющими произвольное количество пар массива:

   FUNCTION Main
   local harr := hb_Hash( "six", 6, "eight", 8, "eleven", 11 )

      harr[10] := "str1"
      harr[23] := "str2"
      harr["fantasy"] := "fiction"

      ? harr[10], harr[23]                                   // str1  str2
      ? harr["eight"], harr["eleven"], harr["fantasy"]       // 8       11  fiction
      ? len(harr)                                            // 6
      ?

      RETURN nil
   
Альтернативный способ инициализации хэш-массива:
   local harr := hb_Hash( "six" => 6, "eight" => 8, "eleven" => 11 )
   

Для хэш массива определены 4 флага:

    - Autoadd - когда установлен в истину (по умолчанию), операции присвоения ( как harr["fantasy"] := "fiction" в вышеприведенном примере ) приводят к добавлению новой пары, когда в ложь - к ошибке;
    - Binary - когда установлен в истину (по умолчанию), сортировка массива производится в соответствии с весом бинарного кода символа, без принятия во внимание национальных кодовых страниц и соответствующих правил сортировки. Это позволяет существенно ускорить манипуляции с хэш массивами;
    - CaseMatch - определяет, учитывать ли Case символа ( большой/маленький ) при сортировке.     - KeepOrder - когда установлен в истину, порядок пар в массиве соответствует очередности их добавления.

Ниже приведена таблица функций для хэш массивов.

aHash := hb_hash( [ Key1, Value1 ], ..., [ KeyN, ValueN ] ) Создание, инициализация хэш массива
lExists := hb_hHaskey( aHash, Key ) Возвращает логическое значение, указывающее, есть ли пара с ключом Key в массиве aHash
xValue := hb_hGet( aHash, Key ) Возвращает значение пары с ключом Key в массиве aHash - то же самое, что xValue := aHash[Key]
xValue := hb_hGetDef( aHash, Key, DefaultVal ) Возвращает значение пары с ключом Key в массиве aHash или DefaultVal, если ключ не найден
hb_hSet( aHash, Key, xValue ) Устанавливает значение пары с ключом Key в массиве aHash - то же самое, что aHash[Key] := xValue
hb_hDel( aHash, Key ) Удаляет пару с ключом Key из массива aHash
nPosition := hb_hPos( aHash, Key ) Возвращает индекс пары с ключом Key в массиве aHash
Key := hb_hKeyAt( aHash, nPosition ) Возвращает ключ пары в массиве aHash с индексом nPosition
xValue := hb_hValueAt( aHash, nPosition, [NewValue] ) Возвращает значение пары в массиве aHash с индексом nPosition и устанавливает новое NewValue, если оно задано
array := hb_hPairAt( aHash, nPosition ) Возвращает двумерный массив ключ/значение из пары в массиве aHash с индексом nPosition
hb_hDelAt( aHash, nPosition ) Удаляет пару из массива aHash с индексом nPosition
aKeys := hb_hKeys( aHash ) Возвращает массив всех ключей массива aHash
aValues := hb_hValues( aHash ) Возвращает массив всех значений массива aHash
hb_hFill( aHash, xValue ) Заполняет массив aHash значениями xValue
aHash2 := hb_hClone( aHash ) Возвращает копию массива aHash
aHash2 := hb_hCopy( aHash2, aHash, [nStart], [nCount] ) Копирует пары из массива aHash в aHash2. Можно указать nStart - стартовая позиция, с которой копировать и nCount - сколько пар копировать
aHash2 := hb_hMerge( aHash2, aHash, bBlock | nPosition ) добавляет пары из массива aHash в aHash2. bBlock - кодоблок, исполняемый для каждой пары источника, ему передаются ключ, значение и индекс. Если bBlock возвращает истину, пара копируется. nPosition - индекс пары, которая будет добавлена в aHash.
aHash := hb_hEval( aHash, bBlock, [nStart], [nCount] ) Выполняет кодоблок для каждой пары массива aHash, кодоблоку передаются ключ, значение и индекс.
nPosition := hb_hScan( aHash, xValue, [nStart], [nCount], [lExact] ) Ищет значение xValue в массиве aHash
aHash2 := hb_hSort( aHash ) Сортирует массив aHash
lPrevFlag := hb_hCaseMatch( aHash, [lFlag] ) Устанавливает флаг "case match" для массива aHash и возвращает его предыдущее значение.
aHash := hb_hSetCaseMatch( aHash, [lFlag] ) Устанавливает флаг "case match" для массива aHash
lPrevFlag := hb_hBinary( aHash, [lFlag] ) Устанавливает флаг "binary" для массива aHash и возвращает его предыдущее значение.
aHash := hb_hSetBinary( aHash, [lFlag] ) Устанавливает флаг "binary" для массива aHash
nPrevFlag := hb_hAutoAdd( aHash, [lFlag] ) Устанавливает флаг "auto add" для массива aHash и возвращает его предыдущее значение.
aHash := hb_hSetAutoAdd( aHash, [lFlag] ) Устанавливает флаг "auto add" для массива aHash
nPrevFlag := hb_hKeepOrder( aHash, [lFlag] ) Устанавливает флаг "keep order" для массива aHash и возвращает его предыдущее значение.
aHash := hb_hSetOrder( aHash, [lFlag] ) Устанавливает флаг "keep order" для массива aHash
hb_hAllocate( aHash, nItems ) Резервирует место для массива aHash в количестве nItems пар.
xPrevDef := hb_hDefault( aHash, DefaultValue ) Устанавливает значение по умолчанию для массива aHash и возвращает предыдущее.

3.9 Регулярные выражения

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

Регулярные выражения - это очень мощное средство для поиска в тексте. Многие современные языки программирования ( Perl, Php, Python, Ruby, Javascript и др. ) имеют их встроенную поддержку. Детально познакомиться с регулярными выражениями можно, например, на pcre.ru.

Далее следует группа сходных по составу параметров функций. Здесь cRegEx - строка с регулярным выражением ( текст или уже откомпилированное ); cString - строка, в которой производится поиск; lCase указывает, следует ли учитывать регистр символов ( по умолчанию - следует - .T. ); что означает параметр lNewLine, мне пока не удалось выяснить, если кто подскажет - буду рад; nMaxMatches - максимальное количество соответствий, которые надо возвращать (по умолчанию - без ограничения - 0). Функции ищут в строке cString фрагменты, соответствующие регулярному выражению, заданному cRegEx. Регулярное выражение может состоять из частей, разделенных круглыми скобками. В этом случае фрагмент строки, соответствующий всему выражению, мы далее называем полным совпадением, а подстроку этого фрагмента, соответствующую части регулярного выражения (выделенной круглыми скобками), мы будем называть суб-совпадением. Так, например:

      s := "Claabrok abbey"   // Строка, где будем искать
      cRegex := "(a+)(b+)"    // Символ '+' в регулярном выражении означает, что
                              // предшествующий символ встречается 1 или более раз
      // Имеем 2 полных совпадения:
      // 1) 'aab', здесь 'aa' - первое суб-совпадение, 'b' - второе
      // 2) 'abb', здесь 'a' - первое суб-совпадение, 'bb' - второе
   
aResult := hb_Regex( cRegex, cString, [lCase], [lNewLine] ) Возвращает одномерный массив, включающий первое полное совпадение и все его суб-совпадения (только сами подстроки).
lResult := hb_RegexLike( cRegEx, cString, [lCase], [lNewLine] ) Возвращает истину, если строка cString соответствует выражению cRegEx.
lResult := hb_RegexHas( cRegEx, cString, [lCase], [lNewLine] ) Возвращает истину, если в строке cString найдено хотя бы одно соответствие выражению cRegEx.
aResult := hb_RegexSplit( cRegEx, cString, [lCase], [lNewLine], [nMaxMatches] ) Функция ищет совпадения в cString, исключает найденные подстроки и возвращает массив из оставшихся частей. Она разделяет строку на части, причем разделителем служат соответствующие cRegEx подстроки.
aResult := hb_RegexAtx( cRegEx, cString, [lCase], [lNewLine] ) Возвращает двумерный массив, включающий первое полное совпадение и все его суб-совпадения, для каждого - массив с соответсвующей подстрокой, начальной и конечной позициями.
aResult := hb_RegexAll( cRegex, cString, [lCase], [lNewLine], [nMaxMatches], [nGetMatch], [lOnlyMatch] ) Здесь первые пять параметров были описаны выше, nGetMatch определяет характер возвращаемого массива, если 0, то функция возвращает и полные совпадения и суб-совпадения, если 1 - только полные совпадения, если 2 - первые суб-совпадения, 3 - вторые суб-совпадения и т.д.; lOnlyMatch - если .T.(по умолчанию), в возвращаемы массив включаются только подстроки, если - .F., то еще и начальная и конечная позиции найденных подстрок. В зависимости от сочетания этих двух параметров возвращаемый массив может быть одномерным, двухмерным или трехмерным. Так, если nGetMatch не равен 0 и lOnlyMatch - истина, то возвращается одномерный массив из подстрок ( полных совпадений или суб-совпадений ). Если nGetMatch равен 0 и lOnlyMatch - истина, то возвращается двумерный массив, каждый элемент которого - массив с подстрокой - полным совпадением и подстроками - суб-совпадениями ). Если же nGetMatch равен 0 и lOnlyMatch - ложь, то возвращается трехмерный массив - вместо каждой подстроки предыдущего случая появляется массив, включающий подстроку, начальную и конечную ее позицию.

И еще 3 функции:

pRegEx := hb_RegexComp( cRegEx, [lCase], [lNewLine] ) Компилируется cRegEx - строка с регулярным выражением, возвращается откомпилированный код; lCase указывает, следует ли учитывать регистр символов ( по умолчанию - следует - .T. ). Это имеет смысл делать для ускорения операций поиска, если данное выражение будет использоваться больше одного раза.
lGood := hb_isRegex( cRegEx ) Возвращает истину, если cRegEx - скомпилированное регулярное выражение.
cMatch := hb_Atx( cRegEx, cString, [lCase], [@nStart], [@nEnd] ) Осуществляет поиск в строке cString соответствия выражению cRegEx; lCase указывает, следует ли учитывать регистр символов ( по умолчанию - следует - .T. ); nStart, nEnd - соответственно, с какой позиции начинать поиск и на какой заканчивать. Возвращает первую найденную подстроку ( или Nil, если ничего не найдено ). В nStart, если он передан по ссылке, записывается позиция найденной подстроки, в nEnd - ее длина.

И, наконец, пример использования простейшего регулярного выражения, уже рассмотренного выше:

   FUNCTION Main()
   Local s := "ClaAbrok aBbey", n1, n2

      ? hb_isregex( hb_regexcomp( "a+b+" ) )                // .T.
      n1 := 1
      n2 := Len( s )
      ? hb_atx( "a+b+", s, .f., @n1, @n2 ), n1, n2          // aAb      3     3
      ? hb_regexhas( "a+b+", s, .f. )                       // .T.
      ? hb_regexhas( "a+b+", s )                            // .F.
      ? hb_regexlike( "a+b+", s, .f. )                      // .F.

      PrintArray( hb_regex( "a+b+",s,.f. ) )                // { aAb }
      PrintArray( hb_regex( "(a+)(b+)",s,.f. ) )            // { aAb aA b }

      PrintArray( hb_regexSplit( "a+b+",s,.f. ) )           // { Cl rok  ey }

      PrintArray( hb_regexAtx( "a+b+",s,.f. ) )             // { { aAb 3 5 } }
      PrintArray( hb_regexAtx( "(a+)(b+)",s,.f. ) )         // { { aAb 3 5 } { aA 3 4 } { b 5 5 } }

      PrintArray( hb_regexAll( "a+b+",s,.f. ) )             // { { aAb aBb } }
      PrintArray( hb_regexAll( "(a+)(b+)",s,.f. ) )         // { { aAb aA b } { aBb a Bb } }
      PrintArray( hb_regexAll( "(a+)(b+)",s,.f.,,,1 ) )     // { aAb aBb }
      PrintArray( hb_regexAll( "(a+)(b+)",s,.f.,,,2 ) )     // { aA a }
      PrintArray( hb_regexAll( "(a+)(b+)",s,.f.,,,0,.f. ) ) 
      // { { { aAb 3 5 } { aA 3 4 } { b 5 5 } } { { aBb 10 12 } { a 10 10 } { Bb 11 12 } } }
      PrintArray( hb_regexAll( "(a+)(b+)",s,.f.,,,1,.f. ) ) // { { aAb 3 5 } { aBb 10 12 } }

      ?
   Return Nil

   Function PrintArray( arr, lNoNewLine )
   Local i

      IF lNoNewLine == Nil .OR. !lNoNewLine
         ?
      ENDIF
      ?? " {"
      FOR i := 1 TO Len( arr )
         IF Valtype( arr[i] ) == "A"
            PrintArray( arr[i], .T. )
         ELSE
            ?? " " + Iif( Valtype( arr[i] ) == "N", Ltrim(Str(arr[i])), arr[i] )
         ENDIF
      NEXT
      ?? " }"
   Return Nil
   

3.10 INET подсистема

Это базовый набор функций, предназначенный для реализации работы в сети по IP протоколу. С их помощью можно создать сокет, установить соединение с другим сокетом ( на этом же или удаленном компьютере ) и организовать прием/передачу данных. Это именно базовый набор, реализующий TCP/IP и UDP протокол. Используя его, можно реализовать обмен по протоколу более высокого уровня - HTTP, FTP, POP3, и т.д. Одним из примеров такой реализации является библиотека hbtip, исходники которой находятся в harbour/contrib/hbtip.

lResult := hb_inetInit() Инициализирует INET подсистему, возвращает .T. в случае успеха. Должна вызываться перед вызовами других INET функций ( в начале программы, например ).
hb_inetCleanup() Освобождает ресурсы, занятые INET подсистемой. Ее надо вызывать в конце программы, использующей INET функции.
hSocket := hb_inetCreate( [nTimeOut] ) Создает и возвращает хэндл сокета ( hSocket ) для последующего подключения к ресурсам в сети; nTimeOut - значение таймаута для сокета в миллисекундах. Таймаут устанавливается для блокирующих операций, таких как чтение и запись данных, т.е. тех, которые останавливают программу, переводят ее в режим ожидания завершения текущей операции. Если время ожидания превышает установленное значение таймаута, операция немедленно завершается и hb_inetErrorCode(hSocket) возвращает -1. Обратите внимание, что речь идет именно о единичной блокирующей операции, а не о функции чтения/записи hb_inet... Некоторые функции, например, hb_inetRecvAll(), могут вызывать такие операции несколько раз, поэтому их время выполнения может превысить установленный таймаут.
По умолчанию значение таймаута равно -1, т.е. ограничение не установлено.
nResult := hb_inetClose( hSocket ) Закрывает ранее созданный сокет с хэндлом hSocket и соответствующее соединение. Возвращает 0 при успехе или -1 в случае ошибки. Если у вас есть другие потоки, которые используют этот сокет, их ожидание завершается и им возвращается ошибка. Эта функция не разрушает сокет, так что другие потоки могут обращаться к нему, чтобы проверить, не закрыт ли он ( и, если да, то завершить соответствующие операции ).
fd := hb_inetFD( hSocket, [l] )  
nResult := hb_inetDestroy( hSocket ) Закрывает и разрушает сокет, после вызова этой функции сокет уже не может использоваться. Возвращает 0 при успехе или -1 в случае ошибки.
nResult := hb_inetStatus( hSocket ) Возвращает 1, если сокет существует, или -1 - в обратном случае.
cResult := hb_inetCRLF() Возвращает последовательность CRLF ( возврат строки + новая строка ), используемую во многих протоколах.
lResult := hb_inetIsSocket( hSocket ) Возвращает .T., если переданный параметр - хэндл сокета.
nMillis := hb_inetTimeout( hSocket [,nMillis] ) Устанавливает новое, если задан второй параметр nMillis, и возвращает старое значение таймаута для сокета hSocket. Подробнее о таймауте см. в описании hb_inetCreate().
hb_inetClearTimeout( hSocket ) Очищает ( устанавливает в -1 ) значение таймаута для сокета hSocket. Подробнее о таймауте см. в описании hb_inetCreate().
nMillis := hb_inetTimeLimit( hSocket [,nMillis] ) Устанавливает новое, если задан второй параметр nMillis, и возвращает старое значение TimeLimit для сокета hSocket. TimeLimit работает, если установлен xCallBack - см. ниже описание hb_inetPeriodCallback().
hb_inetClearTimeLimit( hSocket ) Очищает ( устанавливает в -1 ) значение TimeLimit для сокета hSocket.
nResult := hb_inetErrorCode( hSocket ) Возвращает код завершения последней операции, 0 - в случае успеха, 1 - соединение закрыто, остальные - стандартные коды ошибок, определенные в Winsock или Unixsockets.
cString := hb_inetErrorDesc( hSocket ) Возвращает строку с описанием ошибки, случившейся при исполнении последней операции.
hb_inetClearError( hSocket )
nResult := hb_InetCount( hSocket ) Возвращает количество символов, прочитанных или записанных во время последней операции.
cString := hb_InetAddress( hSocket ) Возвращает адрес удаленного сервера ( или локальный адрес, если сокет принадлежит серверу ) в строковой форме - четыре числа, разделенные точками.
cString := hb_InetPort( hSocket ) Возвращает порт, к которому привязан сокет, или порт удаленного сокета, с которым установлена связь.
xPrevCallback := hb_inetPeriodCallback( hSocket [,xCallback] ) Устанавливает xCallBack для сокета hSocket. Это кодоблок, массив или что-то другое, что может быть исполнено функцией hb_execFromArray() (она описана ниже в подразделе 3.13.14 ). xCallBack исполняется блокирующей операцией, когда истекает таймаут, установленный для этого сокета. Если xCallBack возвращает .F., функция чтения/записи прекращает попытки запустить блокирующую опрерацию и возвращает ошибку. Если же xCallBack возвращает .T., функция повторяет запуск блокирующей операции чтения/записи - и так до тех пор, пока не истечет TimeLimit (см. выше hb_inetTimeLimit()), если он установлен. Если TimeLimit не установлен, цикл продолжается до тех пор, пока чтение/запись не осуществлятся или пока xCallBack не вернет .F..
Итак, чтобы xCallBack запускался, необходимо, чтобы был установлен таймаут для сокета; TimeLimit, если он установлен, должен быть больше, чем таймаут. В этом случае xCallBack будет запускаться с периодичностью, указанной значением таймаута, пока не завершится операция чтения/записи или пока не истечет TimeLimit, если он установлен.
hb_inetClearPeriodCallback( hSocket ) Удаляет xCallBack, установленный с помощью hb_inetPeriodCallback().
nResult := hb_inetGetSndBufSize( hSocket ) Возвращает размер буфера записи, или -1 в случае ошибки.
nResult := hb_inetGetRecvBufSize( hSocket ) Возвращает размер буфера чтения, или -1 в случае ошибки.
nResult := hb_inetSetSndBufSize( hSocket, nSize ) Устанавливает размер буфера записи, или -1 в случае ошибки.
nResult := hb_inetSetRecvBufSize( hSocket, nSize ) Устанавливает размер буфера чтения, или -1 в случае ошибки.

hSocket := hb_inetServer( port [,hSocket [,cBindAddr [,nListenLimit]]] ) Создает сокет и возвращает хэндл hSocket. Это сокет сервера, он может принимать соединения от клиентов на порт port. Параметр cBindAddr служит для указания адреса конкретного интерфейса на компьютере, к которому должен быть привязан этот сервер. Это нужно в тех случаях, если на компьютере работают несколько логических интерфейсов ( 2 и более сетевых карты, PPP, loopback и пр. ) и вам надо, чтобы сервер отвечал на запросы толко по одному из них. nListenLimit обычно не требуется указывать; если на сокет придет nListenLimit попыток соединения от клиентов, а программа еще не успела обработать ни одной из них, то следующая попытка будет отвергнута ядром с сообщением, что сервер занят (busy). Обычно значение по умолчанию ( 10 ) достаточно даже для тяжело загруженного сервера.
hSocket := hb_inetAccept( hSocketSrv ) Ожидает, пока какой-нибудь клиент попытается соединиться с серверным сокетом hSocketSrv, созданным с помощью hb_inetServer(). Возвращает новый сокет, созданный специально для связи с этим клиентом. В случае ошибки возвращается Nil и в hSocketSrv устанавливается код ошибки.
hSocket := hb_inetConnect( cAddress, nPort ) Осуществляет подсоединение к порту nPort сервера ( локального или удаленного ) по адресу cAddress, который может быть передан как IP адрес из 4 чисел, разделенных точками ("192.168.0.1") или как DNS имя хоста ("www.kresin.ru"). Возвращает хэндл вновь созданного сокета hSocket.
hb_inetConnect( cAddress, nPort, hSocket ) Осуществляет подсоединение к порту nPort сервера ( локального или удаленного ) по адресу cAddress, который может быть передан как IP адрес из 4 чисел, разделенных точками ("192.168.0.1") или как DNS имя хоста ("www.kresin.ru"). В отличие от предыдущего варианта этой функции, использует предварительно подготовленный сокет hSocket.
hSocket := hb_inetConnectIP( cAddress, nPort ) Работает аналогично hb_inetConnect(), но в качестве адреса принимает только IP адрес и является thread safe, т.е. ее можно запускать одновременно из разных потоков.
hb_inetConnectIP( cAddress, nPort, hSocket ) Работает аналогично hb_inetConnect(), но в качестве адреса принимает только IP адрес и является thread safe, т.е. ее можно запускать одновременно из разных потоков.

nResult := hb_inetRecv( hSocket, @cResult [,nAmount] ) Читает принимаемые данные из сокета hSocket в предварительно подготовленную строку cResult в количестве, не превышающем nAmount, если этот параметр передан, или длину cResult в противном случае; возвращает количество прочитанных байт. Функция блокирует выполнение программы ( или потока ), пока какие-нибудь данные не будут прочитаны из сокета, или пока не произойдет какая-либо ошибка ( в том числе окончание таймаута ). Она не обязательно заполнит всю строку, не обязательно примет все данные, передаваемые в сокет - в этом случае надо будет вызывать ее опять до окончания полного приема данных. Чтобы заблокировать поток до полного окончания приема данных, используйте функцию hb_inetRecvAll().
nResult := hb_inetRecvAll( hSocket, @cResult [,nAmount] ) Читает принимаемые данные из сокета hSocket в предварительно подготовленную строку cResult в количестве, не превышающем nAmount, если этот параметр передан, или длину cResult в противном случае; возвращает количество прочитанных байт. В отличие от hb_inetRecv() блокирует поток, пока требуемое количество данных не будет прочитано.
cResult := hb_inetRecvLine( hSocket [,@nBytesRead [,nMaxLength [,nBufSize]]] ) Блокирует поток, пока не будет считана последовательность CRLF и возвращает полученную строку. Если произошла ошибка, или сокет закроется до того, как будет считан CRLF, функция не возвратит ничего и будет установлен код ошибки. Возвращаемая строка не включает CRLF. В nBytesRead, если он передан, будет записано количество принятых байт, включая CRLF - т.е., при нормальном завершении это будет длина результирующей строки плюс 2. nMaxLength, если он передан, указывает максимальное количество байт, которое может быть принято, независимо от того, получен ли CRLF. nBufSize - размер буфера приема, по умолчанию - 80 байт. Если принимаемая строка больше, выделяется фрагмент памяти на nBufSize больше и т.д., т.е для длинной строки может много раз потребоваться перераспределение памяти, что не очень хорошо. Так что с помощью указания nBufSize вы можете этим управлять. Можно, например, задать nBufSize равный nMaxLength - тогда сразу будет выделен буфер размером nMaxLength и больше перераспределяться не будет.
cResult := hb_inetRecvEndBlock( hSocket [,cBlock [,@nBytesRead [,nMaxLength [,nBufSize]]]] ) Эта функция ведет себя точно так же, как и hb_inetRecvLine(), но признаком завершения приема здесь служит строка, передаваемая в параметре cBlock. По умолчанию cBlock == CRLF, так что, если этот параметр не задан, функция идентична hb_inetRecvLine().
nResult := hb_inetDataReady( hSocket [,nMillis] ) Проверяет, есть ли в сокете данные, доступные для чтения и возвращает 1, если есть, 0 - если нет и -1 в случае ошибки. Если задан параметр nMillis, функция ждет появления данных в течение nMillis миллисекунд и возвращает результат сразу при появлении данных. Если этот параметр не задан, функция возвращает результат немедленно.
nResult := hb_inetSend( hSocket, cBuffer [,nLength] ) Посылает данные, содержащиеся в строке cBuffer через сокет hSocket, возвращает количество переданных байт, 0 - если сокет был закрыт или -1 в случае ошибки. Параметр nLength, если он передается, указывает, сколько байт надо передать. Учтите, что эта функция не гарантирует, что все данные будут записаны, поэтому надо проверять возвращаемое число.
nResult := hb_inetSendAll( hSocket, cBuffer [,nLength] ) Посылает данные, содержащиеся в строке cBuffer через сокет hSocket, возвращает количество переданных байт, 0 - если сокет был закрыт или -1 в случае ошибки. Параметр nLength, если он передается, указывает, сколько байт надо передать. В отличие от hb_inetSend() эта функция гарантирует, что все данные будут записаны, только после этого она завершит свою работу.

aHosts := hb_inetGetHosts( cName ) Возвращает массив IP адресов, связанных с хостом под названием cName.
aHosts := hb_inetGetAlias( cName ) Возвращает массив алиасов, связанных с хостом под названием cName.
hb_inetInfo()

hSocket := hb_inetDGram( [lBroadCast] ) Создает и возвращает сокет для работы по UDP протоколу. Если lBroadCast установлен в .T., сокет сможет посылать и принимать широковещательные сообщения; в большинстве систем программа должна иметь для этого специальные привилегии.
hSocket := hb_inetDGramBind( nPort [,cAddress [,lBroadCast]] ) Создает и возвращает сокет для работы по UDP протоколу, привязанный к порту nPort и к определенному логическому интерфейсу, описанному cAddress (если он задан). Если lBroadCast установлен в .T., сокет сможет посылать и принимать широковещательные сообщения; в большинстве систем программа должна иметь для этого специальные привилегии.
nBytesSent := hb_inetDGramSend( hSocket, cAddress, nPort, cBuffer [,nSize] ) Посылает пакет данных, содержащихся в cBuffer, через сокет hSocket на IP адрес cAddress, порт nPort. nSize определяет количество передаваемых данных; если этот параметр опущен, передается все, что есть в cBuffer. Возвращается количество переданных байт или -1 в случае ошибки.. Поскольку функция не гарантирует, что все данные будут переданы, возвращаемое значение надо проверять.
nBytesRead := hb_inetDGramRecv( hSocket, @cBuffer [,nSize] ) Читает принимаемые данные из сокета hSocket в предварительно подготовленную строку cResult в количестве, не превышающем nSize, если этот параметр передан, или длину cBuffer в противном случае. Возвращает количество прочитанных байт или -1 в случае ошибки.

Предлагаю вашему вниманию полезный :) пример использования описанных выше (INET) и ниже (многопоточность) функций. Итак, двухпоточное приложение, проверяющее каждые 2 минуты наличие обновлений на форуме clipper.borda.ru ( не забудьте, что его надо откомпилировать с поддержкой многопоточности, иначе оно не будет работать ):

   #include "box.ch"

   REQUEST HB_CODEPAGE_RU866
   REQUEST HB_CODEPAGE_RU1251

   Static mutex1
   Static aNew := {}

   Function Main()
   Local pThread, lEnd := .F., nkey

      hb_cdpSelect( "RU866" )
      hb_inetInit()

      // Создаем mutex для синхронизации потоков при обращении к массиву aNew
      mutex1 := hb_mutexCreate()
      // Создаем поток для проверки обновлений с clipper.borda.ru
      pThread := hb_threadStart( @GetData(), @lEnd )

      // Просто ждем пользовательского ввода.
      CLEAR SCREEN
      @ 24, 1 SAY "Нажмите Esc, чтобы завершить программу, F5 - изменения"
      read
      DO WHILE ( nKey := Inkey(0) ) != 27
         IF nKey == -4    // F5
            ShowUpd()
         ENDIF
      ENDDO

      // Посылаем посредством lEnd сигнал завершения потоку и ждем его.
      lEnd := .T.
      hb_threadJoin( pThread )

      hb_inetCleanup()

   Return .T.

   // Эта функция вызывается по F5 и показывает изменения на сайте
   Function ShowUpd()
   Local arr, bufc, i, l

      // Вот тут нам понадобился mutex - мы читаем массив aNew, который мог бы в это
      // время модифицироваться вторым потоком
      hb_mutexLock( mutex1 )
      IF ( l := !Empty( aNew ) )
         arr := {}
         FOR i := 1 TO Len( aNew )
            Aadd( arr, Padr( hb_translate( Trim(aNew[i,2])+": " ;
               +Trim(aNew[i,3])+" "+Trim(aNew[i,4]),"RU1251","RU866" ),64 ) )
         NEXT
      ENDIF
      hb_mutexUnLock( mutex1 )


      bufc := Savescreen( 7,7,13,73 )
      @ 7,7,13,73 BOX B_DOUBLE_SINGLE + Space(1)
      IF l
         AChoice( 8,8,12,72,arr )
      ELSE
         @ 10, 35 SAY "Ничего нового..."
         Inkey(1)
      ENDIF
      Restscreen( 7,7,13,73, bufc )
      hb_dispOutAt( 0, 69, "          ", "GR+/N" )

   Return .T.

   // Эта функция анализирует главную страницу сайта и ищет там нужные изменения.
   // Чтобы лучше ее понять, просмотрите исходный текст главной страницы clipper.borda.ru
   Static Function CheckAns( cBuf )
   Local nPos1 := 1, nPos2, aItems, aRes := {}
   Local aStru := { {"ID","C",2,0}, {"NAME","C",16,0}, {"TM","C",10,0}, {"LOGIN","C",16,0}, {"TITLE","C",32,0} }
   Local fname := "cl_borda.dbf", lFirstTime := .F.
   Field ID, NAME, TM, LOGIN, TITLE

      IF !File( fname )
         dbCreate( fname, aStru )
         lFirstTime := .T.
      ENDIF
      USE (fname) NEW EXCLUSIVE
     
      DO WHILE ( nPos1 := hb_At( "st(", cBuf, nPos1 ) ) != 0
         IF ( nPos2 := hb_At( ")", cBuf, nPos1 ) ) != 0

            aItems := hb_aTokens( Substr(cBuf,nPos1+3,nPos2-nPos1-3), ",", .T. )
            aItems[1] := Padr( Substr( aItems[1], 2, Min( aStru[1,3],Len(aItems[1])-2 ) ), aStru[1,3] )
            aItems[2] := Padr( Substr( aItems[2], 2, Min( aStru[2,3],Len(aItems[2])-2 ) ), aStru[2,3] )
            aItems[5] := Padr( Substr( aItems[5], 2, Min( aStru[3,3],Len(aItems[5])-2 ) ), aStru[3,3] )
            aItems[8] := Padr( Substr( aItems[8], 2, Min( aStru[4,3],Len(aItems[8])-2 ) ), aStru[4,3] )
            aItems[9] := Padr( Substr( aItems[9], 2, Min( aStru[5,3],Len(aItems[9])-2 ) ), aStru[5,3] )

            IF lFirstTime
               APPEND BLANK
               REPLACE ID WITH aItems[1], NAME WITH aItems[2], TM WITH aItems[5], ;
                     LOGIN WITH aItems[8], TITLE WITH aItems[9]
            ELSE
               LOCATE FOR ID == aItems[1]
               IF !Found()
                  APPEND BLANK
                  REPLACE ID WITH aItems[1], NAME WITH aItems[2], TM WITH aItems[5], ;
                        LOGIN WITH aItems[8], TITLE WITH aItems[9]
                  Aadd( aRes, {aItems[1],aItems[2],aItems[8],aItems[9]} )
               ELSEIF TM != aItems[5] .OR. LOGIN != aItems[8] .OR. TITLE != aItems[9]
                  REPLACE TM WITH aItems[5], LOGIN WITH aItems[8], TITLE WITH aItems[9]
                  Aadd( aRes, {aItems[1],aItems[2],aItems[8],aItems[9]} )
               ENDIF
            ENDIF

            nPos1 := nPos2 + 1
         ELSE
            EXIT
         ENDIF
      ENDDO
      USE

   Return aRes

   // Это 2-й поток
   Function GetData( lEnd )
   Local nCount := 0, hSocket, cUrl, cServer, cBuf, aRes

      cServer := "clipper.borda.ru"
      cURL := "GET http://" + cServer + "/ HTTP/1.1" +Chr(13)+Chr(10)
      cURL += "Host: " + cServer + Chr(13)+Chr(10)
      cURL += "User-Agent: test_util"+Chr(13)+Chr(10)
      cUrl += Chr(13)+Chr(10)

      DO WHILE !lEnd
         IF nCount == 0
            hb_dispOutAt( 0, 61, "Читаем.", "GR+/N" )
            hSocket := hb_inetCreate()                   // создаем сокет
            hb_inetConnect( cServer, 80, hSocket )       // присоединяемся к сайту форума
            IF hb_inetErrorCode( hSocket ) != 0
               hb_dispOutAt( 0, 61, "Сбой...", "GR+/N" ) 
            ENDIF

            // Посылаем сформированный выше запрос и ждем ответа.
            IF hb_inetSendAll( hSocket, cURL ) > 0 .AND. !Empty( cBuf := hb_inetRecvEndBlock( hSocket, "main2(",,,4096 ) )
               IF !Empty( aRes := CheckAns( cBuf ) )
                  hb_dispOutAt( 0, 69, "!! Есть !!", "GR+/N" )
                  // Используем mutex для безопасной модификации aNew
                  hb_mutexLock( mutex1 )
                  aNew := aRes
                  hb_mutexUnLock( mutex1 )
               ENDIF
               hb_dispOutAt( 0, 61, "       ", "GR+/N" )
            ELSE
               hb_dispOutAt( 0, 61, "Сбой...", "GR+/N" )
            ENDIF

            // Закрываем сокет
            hb_inetClose( hSocket ) 
         ENDIF
         hb_idleSleep(2)
         IF ++nCount >= 60
            nCount := 0
         ENDIF
      ENDDO

   Return Nil
   

При смене движка форума эта программа может перестать работать, потому что она предполагает наличие определенных строк в html коде - ищет вызовы javascript функций main2() и st().

3.11 Многопоточность

Многопоточность - это модель программирования, которая позволяет нескольким потокам выполняться в рамках одного процесса, одного приложения, взаимодействуя между собой, разделяя ресурсы этого приложения. Потоки выполняются операционной системой параллельно. Если система однопроцессорная, то это квазипараллельность, обеспечиваемая тем, что ОС распределяет время между потоками. В многопроцессорной системе потоки могут реально выполняться параллельно на разных процессорах, обеспечивая общее повышение производительности приложения. Часто бывает удобно использовать отдельные потоки для каких-то действий, связанных с длительными вычислениями или с длительным ожиданием какого-либо события, блокирующим выполнение программы ( например, при работе в сети с использованием сокетов ); при этом основной поток продолжает реагировать на действия пользователя.

Harbour - приложение может быть собрано для однопоточного выполнения и для многопоточного. В последнем случае необходимо использовать многопоточную версию виртуальной машины Harbour - библиотеку hbvmmt.lib вместо однопоточной hbvm.lib и библиотеку вашего C компилятора, содержащую функции для реализации многопоточности (для Borland C это cw32mt.lib). Самое простое решение - использовать для сборки вашего приложения hbmk2 с ключом -mt. Ничто не мешает вам собирать в многопоточном режиме и обычные однопоточные приложения, но так они будут работать чуть медленнее и займут чуть больше места на диске и в памяти.

При создании новый поток наследует от родителя:
    - кодовую страницу, установленную hb_cdpSelect(),
    - язык ( hb_langSelect() ),
    - все SET установки,
    - RDD по умолчанию,
    - GT драйвер и консольное окно,
    - установки I18N.

Эти установки инициализируются в начальные значения ( создается новая их копия ):
    - публичная переменная Getlist := {},
    - обработчик ошибок, инициализируемый вызовом Errorsys(),
    - обработчик математических ошибок,
    - установки макрокомпилятора ( устанавливаемые hb_setMacro() ),
    - RDDI_* установки в стандартных RDD ( сторонние RDD могут использовать глобальные установки ),
    - thread static переменные.

Отдельные Public и Private переменные могут передаваться потоку при его создании.

Следующие ресурсы используются совместно:
    - функции и процедуры,
    - определения классов,
    - модули RDD,
    - GT драйверы,
    - языковые модули,
    - модули кодовых страниц,
    - статические переменные.

Локальные ресурсы потока:
    - Public и Private переменные, кроме тех, что используются совместно,
    - рабочие области (workareas),
    - thread static переменные.

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

pThID := hb_threadStart( @sStart() | bStart | cStart [, params,... ] ) Создает новый поток, возвращает указатель потока pThID, или Nil, если поток создать не удалось. Первым параметр hb_threadStart() определяет, какой код будет исполнять новый поток. Это может быть указатель на функцию, кодоблок или имя функции, передаваемое как текстовая строка. Далее следует список параметров, передаваемых этой функции.
pThID := hb_threadStart( nThreadAttrs, @sStart() | bStart | cStart [, params,... ] ) Этот вариант использования hb_threadStart() отличается от предыдущего наличием первого параметра nThreadAttrs, содержащего атрибуты потока. Эти атрибуты описаны в hbthread.ch:
   #define HB_THREAD_INHERIT_PUBLIC    1
   #define HB_THREAD_INHERIT_PRIVATE   2
   #define HB_THREAD_INHERIT_MEMVARS   3
   #define HB_THREAD_MEMVARS_COPY      4
        
Если мы, например, пишем hb_threadStart( hb_bitor(HB_THREAD_INHERIT_PUBLIC,HB_THREAD_MEMVARS_COPY),@thFunc() ), то поток получает копии всех Public переменных родителя, а если hb_threadStart( HB_THREAD_INHERIT_MEMVARS,@thFunc() ), то поток разделяет все Public и Private переменные с родителем, см. пример harbour/tests/mt/mttest08.prg.
pThID := hb_threadSelf() Возвращает указатель потока, из которого вызывается эта функция. Может возвратить Nil, если поток создан не средствами Харбора.
nThNo := hb_threadId( [ pThID ] ) Возвращает идентификатор потока по его указателю.
lOk := hb_threadJoin( pThID [, @xRetCode ] ) Приостанавливает выполнение текущего потока, пока не завершится поток, на который указывает pThID.
lOk := hb_threadDetach( pThID ) Отключает поток, на который указывает pThID, делает его отдельным. Теперь его уже не надо присоединять функцией hb_threadJoin(), он освободит все свои ресурсы автоматически после завершения своей работы.
lOk := hb_threadQuitRequest( pThID ) Посылает операционной системе запрос завершить поток, на который указывает pThID и приостанавливает текущий поток, ожидая выполнения завершения потока pThID. Учтите, что при этом потока "убивается" извне, и результат его работы неопределен.
hb_threadTerminateAll() Посылает запрос на завершение всех потоков и ждет, пока это не произойдет. Функция может быть вызвана только из главного потока.
hb_threadWaitForAll() Ждет, пока не завершатся все потоки.
nRes := hb_threadWait( pThID | apThID [, nTimeOut] [, lAll] ) Ожидает nTimeOut секунд ( или неограниченное время, если этот параметр не задан ) завершения потока pThID, или одного из потоков в массиве указателей apThID, или всех потоков из этого массива, если указан параметр lAll и он равен .T.. Возвращает номер завершившегося потока, или количество потоков, завершившихся за nTimeOut секунд, если lAll равен .T..
lOk := hb_mtvm() Возвращает истину, если ваша программа скомпилирована с возможностью создавать потоки.

Пользуясь этими функциями, мы уже можем написать первые несложные многопоточные приложения, например вот такое, для вывода часов на экран:

   FUNCTION Main()
      LOCAL cVar := Space( 20 )

      CLEAR SCREEN

      IF !hb_mtvm()
         ? "Отсутствует поддержка многопоточности, часов не будет видно."
         WAIT
      ELSE
         hb_threadStart( @Show_Time() )
      ENDIF

      @ 10, 10 SAY "Введите что-нибудь:" GET cVar
      READ
      SetPos( 12, 0 )
      ? "Вы ввели -> [" + cVar + "]"
      WAIT

      RETURN Nil

   FUNCTION Show_Time()
      LOCAL cTime

      DO WHILE .T.
         cTime := Dtoc( Date() ) + " " + Time()
         hb_dispOutAt( 0, MaxCol() - Len( cTime ) + 1, cTime, "GR+/N" )
         hb_idleSleep( 1 )
      ENDDO

      RETURN nil
   

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

Если бы использовалась друга функция, например, DispOut(), курсор перемещался бы из области ввода в основном потоке, при этом не помогло бы сохранение и восстановление его позиции в Show_Time(). Дело в том, что операционная система может прервать выполнение одного потока и передать управление другому в любой момент времени, посреди любой операции. Вы, например, восстанавливаете положение курсора функцией SetPos(), но на любой стадии ее выполнения ( а это длинная серия операций машинного кода ) операционная система может прервать ее и вернуть управление главному потоку или какому-либо другому, который в этот момент тоже модифицирует положение курсора - и результат будет непредсказуем. Поэтому в тех случаях, когда потоки используют какие-либо общие ресурсы ( чаще всего - переменные ), необходимы средства синхронизации работы потоков, чтобы они не обращались к общим ресурсам одновременно. Такими средствами являются семафоры и одна из их разновидностей - mutex ( их называют по-русски "мьютексы", но мне такое написание что-то не нравится ). Смотрим ниже описание соответствующих функций:

pMtx := hb_mutexCreate() Создает mutex и возвращает его хэндл.
lLocked := hb_mutexLock( pMtx [, nTimeOut] ) Блокирует mutex pMtx для текущего потока. Если этот mutex уже заблокирован другим потоком, то текущий поток приостанавливается, пока тот поток не разблокирует mutex pMtx, или пока не истечет nTimeOut миллисекунд, если этот параметр передан. Возвращает .T., если pMtx заблокирован.
lOk := hb_mutexUnlock( pMtx ) Разблокирует предварительно заблокированный mutex pMtx.
hb_mutexNotify( pMtx [, xVal] ) Посылает уведомление очередному потоку, "подписавшемуся" на mutex pMtx с помощью функции hb_mutexSubscribe() или hb_mutexSubscribeNow() и, таким образом, дает ему возможность продолжить работу. Если на pMtx "подписаны" несколько потоков, операционная система сама выбирает, какому из них послать уведомление. Параметр xVal передается разблокируемому потоку, через функцию hb_mutexSubscribe() этого потока, которая записывает xVal в переданный ей по ссылке третий параметр.
hb_mutexNotifyAll( pMtx [, xVal] ) Аналогична hb_mutexNotify(), но, в отличие от нее, посылает уведомление не одному, а всем потокам, "подписанным" на mutex pMtx.
lSubscribed := hb_mutexSubscribe( pMtx, [nTimeOut] [, @xVal] ) Функция приостанавливает выполнение текущего потока на время nTimeOut миллисекунд ( или на неограниченное время, если этот параметр не указан ), пока другой поток не вызовет hb_mutexNotify() или hb_mutexNotifyAll() для того же mutex pMtx. В переданный по ссылке xVal записывается полученное от hb_mutexNotify() значение ( второй параметр hb_mutexNotify() ). Функция возвращает .T., если она завершилась в результате получения уведомления, и .F., если - по таймауту.
lSubscribed := hb_mutexSubscribeNow( pMtx, [nTimeOut] [, @xSubscribed] ) Аналогична предыдущей функции hb_mutexSubscribe(), но, в отличие от нее, игнорирует уведомления, посланные вызовами hb_mutexNotify() до момента начала работы этой функции.
xResult := hb_mutexEval( pMtx, bCode | @sFunc() [, params,...] ) Последовательно выполняет hb_mutexLock( pMtx ), действие, заданное кодоблоком bCode или указателем @sFunc() с параметрами params,... и hb_mutexUnlock( pMtx ).
hb_mutexQueueInfo( pMtx, @nWaiters, @nEvents ) Предоставляет информацию об очереди для mutex pMtx. В nWaiters записывается количество потоков, "подписанных" на pMtx, в nEvents - количество уведомлений, еще не дошедших до "подписавшихся".

Итак, для предотвращения одновременого обращения разных потоков к одному и тому же ресурсу делаем следующее:
    1) создаем предварительно mutex: pMtx := hb_mutexCreate()
    2) в тех местах программы, где происходит обращение к защищаемым ресурсам, оборачиваем соответствующий код блокировкой/разблокировкой mutex'а, т.е. ставим перед перед ним hb_mutexLock( pMtx ) и после него hb_mutexUnLock( pMtx ) - точно так же, как мы блокируем/разблокируем запись расшаренной базы данных при записи в нее. Не забывайте, что, в отличие от rlock(), hb_mutexLock() приостанавливает дальнейшее выполнение потока, если mutex уже чем-то заблокирован - при невнимательности можно получить deadlock - ситуацию, когда несколько потоков взаимно заблокировали выполнение друг друга.

Правда, в нашем примере с часами, на тот случай, если бы мы использовали Setpos() и Dispout(), mutex'ы нам бы не помогли. В главном потоке управление курсором производится не непосредственно в нашей программе, а где-то внутри Харборовской RTL, в реализации функции Readmodal() - и вставить туда блокировку mutex'а мы не можем. Поэтому расширим немного этот пример, добавив туда установку параметров отображения даты:

   #include "box.ch"

   Static mutex1
   Static cOpt := "DD.MM.YY"

   FUNCTION Main()
      LOCAL cVar := Space( 20 )

      CLEAR SCREEN

      IF !hb_mtvm()
         ? "Отсутствует поддержка многопоточности, часов не будет видно."
         WAIT
      ELSE
         mutex1 := hb_mutexCreate()
         hb_threadStart( @Show_Time() )
      ENDIF

      SET KEY -4 TO Options
      @ 10, 10 SAY "Введите что-нибудь:" GET cVar
      READ
      SET KEY -4 TO
      SetPos( 12, 0 )
      ? "Вы ввели -> [" + cVar + "]"
      WAIT

      RETURN Nil

   FUNCTION Show_Time()
      LOCAL cTime

      DO WHILE .T.
         hb_mutexLock( mutex1 )
         cTime := Iif( !Empty( cOpt ), hb_Dtoc( Date(),cOpt ), Space(10) ) + " " + Time()
         hb_mutexUnLock( mutex1 )
         hb_dispOutAt( 0, MaxCol() - Len( cTime ) + 1, cTime, "GR+/N" )
         hb_idleSleep( 1 )
      ENDDO

      RETURN nil

   Function Options
   Local bufc, n, aOpt := { "DD.MM.YYYY","DD/MM/YYYY","DD.MM.YY","YYYY.MM.DD","       " }

      bufc := Savescreen( 7,31,13,49 )
      @ 7,31,13,49 BOX B_DOUBLE_SINGLE + Space(1)
      IF ( n := AChoice( 8,32,12,48,aOpt ) ) != 0
         hb_mutexLock( mutex1 )
         cOpt := aOpt[n]
         hb_mutexUnLock( mutex1 )
      ENDIF
      Restscreen( 7,31,13,49, bufc )

   Return Nil
   

Итак мы имеем переменную cOpt, которая содержит строку формата отображения даты, функцию Options(), которая вызывается по нажатию клавиши F5 и изменяет эту строку формата. Поскольку поток, выполняющий Show_Time(), использует эту строку, то для исключения коллизий мы используем mutex.

Еще один пример многопоточной программы с mutex'ами смотрите в предыдущем разделе.

Еще 2 функции для синхронизации действий между потоками:

lFirstCall := hb_threadOnce( @onceControl [, bAction] ) Исполняет bAction один раз, onceControl - переменная, которая хранит статус выполнения, она должна быть инициализирована в Nil, обычно это статическая переменная. Когда bAction выполняется потоком, все остальные потоки, вызывающие в этот момент hb_threadOnce(), приостанавливаются, даже если они используют другую onceControl. Если поток пытается вызвать hb_threadOnce() с той же onceControl рекурсивно из bAction, то функция сразу завершается, не вызывая bAction и возвращает .F.. Функция возвращает логическое значения, показывающее, был ди это первый вызов hb_threadOnce() для этой onceControl.
lInit := hb_threadOnceInit( @item, value ) Эта функция похожа на hb_threadOnce(), она записывает value в nItem, если nItem равен Nil.

Теперь о доступе к базам данных в многопоточных приложениях. Как уже отмечалось выше, в Harbour рабочие области (workareas) локальны для потока, т.е. каждый поток имеет свои независимые рабочие области и алиасы и, в общем случае, если вам в потоке "Б" нужна база данных, уже открытая в потоке "А", вам надо открыть ее в "Б" по новой, что нетрудно сделать в shared mode.

Но есть и другая возможность - вы можете передать рабочую область от одного потока другому, используя для этого известное пользователям xBase++ zero space. Для этого предназначены следующие 2 функции:

lOk := hb_dbDetach( [nWorkArea|cAlias] [, xCargo] ) Отсоединяет рабочую область, заданную по номеру nWorkArea или по имени алиаса cAlias от текущего потока, перемещая ее в zero space. При этом можно передать информацию потокам, которые будут ее использовать, через xCargo.
lOk := hb_dbRequest( [cAlias] [, lNewArea] [,@xCargo] [,lWait]) Запрашивает рабочую область из zero space по имени алиаса cAlias; lNewArea указывает, использовать ли новую рабочую область ( как и в dbUseArea() ), lWait - следует ли ждать, пока запрошенная рабочая область станет доступна. Функция возвращает .T., если все прошло успешно.

Этот механизм позволяет разным потокам совместно использовать рабочие области. Для иллюстрации введем 2 команды:

      #xcommand UNLOCK WORKAREA [] => hb_dbDetach(  )
      #xcommand LOCK WORKAREA    => hb_dbRequest( , .T.,, .T. )
   

После открытия таблицы командой Use поток выполняет UNLOCK WORKAREA и, т.о., перемещает ее в zero space. Теперь каждый поток, которому нужна эта таблица, может получить к ней доступ, например:

      LOCK WORKAREA "DOCUMENTS"
         COUNT TO nInvoices FOR year( DOCUMENTS->DATE ) == year( date() )
      UNLOCK WORKAREA
   

Другие примеры многопоточных программ вы можете найти в harbour/tests/mt.

3.12 API файлового ввода/вывода (File IO API)

Как известно, Harbour унаследовал от Clipper набор функций FOpen(), FSeek(), FRead(), ..., предназначенных для работы с файлами. Этот набор пополнился новыми функциями, полный их список приведен в подразделе 3.13.2 Файловые функции.

Новое API файлового ввода/вывода - это набор функций с префиксом hb_vf, аналогичных по набору параметров и производимым операциям обычным файловым функциям. Изменена реализация этих функций, что позволило использовать заменяемые драйверы наподобие RDD. Теперь, если перед именем файла поставить префикс какого-либо из существующих драйверов с двоеточием в конце ( "mem:", "net:", ... ), обращение к этому файлу пойдет через этот драйвер. Вот этот вызов, например:

      hb_vfCopyFile( "test.txt", "mem:test.txt" )
создат копию файла "test.txt" в памяти (hbmemio драйвер).

Драйверы ввода/вывода - это дополнительные библиотеки Harbour, такие как hbnetio, hbmemio, hbgzio, hbbz2io, hbcomio и др.

Список функций:

lOk := hb_vfExists( cFileName, [ @cDestFileName ] ) Проверяет, существует ли файл cFileName, аналог File().
nResult := hb_vfErase( cFileName ) Удаляет файл cFileName, аналог FErase().
nResult := hb_vfRename( cFileSrc, cFileDst ) Переименовывает файл cFileSrc в cFileDst, аналог FRename().
nResult := hb_vfCopyFile( cFileSrc, cFileDst ) Копирует файл cFileSrc в cFileDst, аналог FCopy().
lExists := hb_vfDirExists( cDirName )Проверяет, существует ли каталог cDirName, аналог DirExists().
nSuccess := hb_vfDirMake( cDirName ) Создает каталог cDirName, аналог Makedir().
nSuccess := hb_vfDirRemove( cDirName ) Удаляет каталог cDirName, аналог DirRemove().
aDirectory := hb_vfDirectory( [ cDirSpec ], [ cAttr ] ) Возвращает массив с информацией о каталоге cDirName, аналог Directory().
nFreeSpace := hb_vfDirSpace( cDirName, [ nInfoType ] )  
lOk := hb_vfAttrGet( cFileName, @nAttr ) Записывает в переменную nAttr, передаваемую по ссылке, аттрибута файла с именем cFileName; аналог hb_fGetAttr().
lOk := hb_vfAttrSet( cFileName, nAttr ) Устанавливает атрибуты файла cFileName, указанные в nAttr; аналог hb_fSetAttr().
lOk := hb_vfTimeGet( cFileName, @tsDateTime ) Записывает в tsDateTime дату и время модификации файла cFileName; частичный аналог hb_fGetDateTime().
lOk := hb_vfTimeSet( cFileName, tsDateTime ) Устанавливает дату и время модификации файла cFileName; частичный аналог hb_fSetDateTime().
nSuccess := hb_vfLink( cExistingFileName, cNewFileName ) Создает жесткую ссылку на файл, аналог hb_fLink().
nSuccess := hb_vfLinkSym( cTargetFileName, cNewFileName ) Создает символическую ссылку cNewFileName на файл cTargetFileName, аналог hb_fLinkSym().
cDestFileName := hb_vfLinkRead( cFileName ) Возвращает значение (полный путь) символической ссылки, аналог hb_fLinkRead().
pHandle := hb_vfOpen( [@]cFileName, [ nModeAttr ] ) Открывает файл cFileName, аналог FOpen().
lOk := hb_vfClose( pHandle ) Закрывает предварительно открытый файл, аналог FClose().
lOk := hb_vfLock( pHandle, nStart, nLen, [ nType ] ) Устанавливает блокировку на участок файла со смещения nOffsetFrom до nOffsetTo, аналог hb_FLock().
lOk := hb_vfUnlock( pHandle, nStart, nLen ) Снимает блокировку с участка файла со смещения nOffsetFrom до nOffsetTo, аналог hb_FUnLock().
nPID := hb_vfLockTest( pHandle, nStart, nLen, [ nType ] ) 
nRead := hb_vfRead( pHandle, @cBuff, [ nToRead ], [ nTimeOut ] ) Читает фрагмент открытого файла, начиная с текущего положения, аналог FRead().
cBuffer := hb_vfReadLen( pHandle, nToRead, [ nTimeOut ] ) Читает nToRead байт из файла, возвращает прочитанный буфер, аналог hb_FReadLen().
nWritten := hb_vfWrite( pHandle, cBuff, [ nToWrite ], [ nTimeOut ] ) Производит запись в открытый файл, начиная с текущего положения, аналог FWrite().
nRead := hb_vfReadAt( pHandle, @cBuff, [ nToRead ], [ nAtOffset ] ) Читает фрагмент открытого файла, начиная с заданного положения nAtOffset.
nWritten := hb_vfWriteAt( pHandle, cBuff, [ nToWrite ], [ nAtOffset ] ) Производит запись в открытый файл, начиная с заданного положения nAtOffset.
nOffset := hb_vfSeek( pHandle, nOffset, [ nWhence ] ) Перемещает указатель на заданную позицию в открытом файле, аналог FSeek().
lOk := hb_vfTrunc( pHandle, [ nAtOffset ] )  
nSize := hb_vfSize( pHandle | cFileName [, lUseDirEntry ] ) Возвращает размер файла cFileName, аналог hb_FSize().
lEof := hb_vfEof( pHandle )  
hb_vfFlush( , [ lDirtyOnly ] )  
hb_vfCommit( pHandle )  
nResult := hb_vfConfig( pHandle, nSet, [ nParam ] )  
nOsHandle := hb_vfHandle( pHandle )  
pHandle := hb_vfTempFile( @cFileName, [ cDir ], [ cPrefix ], [ cExt ], [ nAttr ] ) Создает временный файл, аналог hb_fTempCreateEx().
cFileBody := hb_vfLoad( cFileName, [ nMaxSize ] ) Читает весь файл (но не больше, чем nMaxSize, если этот параметр указан), аналог hb_Memoread().

3.13 Всякие новые функции

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

3.13.1 Встроенный компилятор

Три функции, использующие встроенный компилятор (hbcplr.lib), основанный на том же коде, что и сам harbour.exe.

nRetCode := hb_compile( "harbour", cFileName, [...] ) Компилирует файл cFileName с переданными ей параметрами компиляции. Результатом ее работы является .c файл.
cHrb := hb_compileBuf( "harbour", cFileName, [...] ) В отличие от hb_compile(), не создает .c файл, а возвращает созданный в результате компиляции p-code в виде текстовой строки, который можно сохранить как hrb файл или исполнить с помощью hb_hrbRun()
cHrb := hb_compileFromBuf( cPrg, "harbour", [...] ) Компилирует код из буфера cPrg, возвращает p-code, как и hb_compileBuf()
Ниже приведен пример использования run-time компиляции:
      FUNCTION Main( cFileName )
      Local handle, buf, cBuf := "", i

         buf := hb_compilebuf( "harbour", cFileName, "/n","/w" )  // Компилируем
         hb_hrbRun( buf )                                         // исполняем

         handle := FCreate( Iif((i:=Rat('.',cFileName))=0,cFileName,Substr(cFileName,1,i-1)) + ".hrb" )
         FWrite( handle, buf )
         FClose( handle )

      Return Nil
   

3.13.2 Файловые функции

lSuccess := hb_fGetAttr( cFileName, @nAttr ) Записывает в переменную nAttr, передаваемую по ссылке, аттрибута файла с именем cFileName.
lSuccess := hb_fSetAttr( cFileName, nAttr ) Устанавливает атрибуты файла cFileName, указанные в nAttr.
lSuccess := hb_fGetDateTime( cFileName, @dDate [, @cTime] ) Записывает в dDate и cTime, соответственно, дату и время модификации файла cFileName.
lSuccess := hb_fSetDateTime( cFileName, dDate [, cTime] ) Устанавливает дату и время модификации файла cFileName.
hb_fcommit( nHandle )  
hb_fisdevice( nHandle )  
nSuccess := hb_fLink( cExistingFileName, cNewFileName ) Создает жесткую ссылку cNewFileName на файл cExistingFileName (работает в Unix/Linux и в Windows начиная с Vista).
cDestFileName := hb_fLinkRead( cFileName ) Возвращает значение (полный путь) символической ссылки (работает в Unix/Linux и в Windows начиная с Vista).
nSuccess := hb_fLinkSym( cTargetFileName, cNewFileName ) Создает символическую ссылку cNewFileName на файл cTargetFileName (работает в Unix/Linux и в Windows начиная с Vista).
hb_flock( nHandle, nOffsetFrom, nOffsetTo ) Устанавливает блокировку на участок файла со смещения nOffsetFrom до nOffsetTo.
hb_funlock( nHandle, nOffsetFrom, nOffsetTo ) Снимает блокировку с участка файла со смещения nOffsetFrom до nOffsetTo.
cReadBuf := hb_freadlen( nHandle, nToRead ) Читает nToRead байт из файла, возвращает прочитанный буфер.
hb_fsetdevmode()  
hb_fsize( cFileName[, lUseDirEntry] ) Возвращает размер файла cFileName.
nHandle := hb_fTempCreate( [cPath], [cPrefix], [nAttr], [@cFileName] ) Создает временный файл, с атрибутами nAttr, с префиксом cPrefix, путь к которому указан в cPath. Функция возвращает handle открытого файла, а в cFileName, передаваемый по ссылке, записывает его имя. Атрибуты файла ( их символьные имена определены в fileio.ch ) могут иметь следующие значения:
           FC_NORMAL          0
           FC_READONLY        1
           FC_HIDDEN          2
           FC_SYSTEM          4
        
nHandle := hb_fTempCreateEx( [@cFileName], [cPath], [cPrefix], [cExt], [nAttr] ) Делает то же, что и предыдущая функция, hb_fTempCreate(), отличается от нее тем, что позволяет задать и расширение создаваемого файла cExt. Ну и порядок следования аргументов другой.
lResult := hb_FileDelete( cFileMask [, cAttr ] ) 

3.13.3 Функции для манипуляций с именами файлов

Группа функций, манипулирующих строкой с путем, именем, расширением файла.
cRes := hb_FNameDir( cFullPath ) Возвращает путь к файлу.
cRes := hb_FNameName( cFullPath ) Возвращает имя файла ( без пути и расширения ).
cRes := hb_FNameExt( cFullPath ) Возвращает расширение файла.
cRes := hb_FNameNameExt( cFullPath ) Возвращает имя и расширение файла
cRes := hb_FNameExtSet( cFullPath, cExt ) Устанавливает новое расширение файла, возвращает новый полный путь.
cRes := hb_FNameExtSetDef( cFullPath, cExt ) Устанавливает новое расширение файла если оно отсутствует, возвращает новый полный путь.

3.13.4 Строковые функции

cString := hb_strFormat( cFormat, ... ) Возвращает строку, форматированную в C-стиле, как в printf().
nResult := hb_HexToNum( cHex ) Функция аналогична Val(), но строка cHex содержит 16-ричное число.
cHex := hb_NumToHex( num[, nLen] ) Преобразует число num в строку длиной nLen в 16-ричной системе.
nTokens := hb_TokenCount( cString, [cDelim], [lSkipStrings], [lDoubleQuoteOnly])Эта и последующие 3 функции разбирают строку cString на элементы, разделителем служит cDelim ( по умолчанию - пробел " " ). Пробел в качестве разделителя имеет ту особенность, что несколько пробелов подряд считаются как один. Параметр lSkipStrings, если он установлен в .T., указывает, что при разбиении на элементы следует пропускать строки, т.е., если разделитель находится между кавычками, двойными или одинарными, он не считается как разделитель; lDoubleQuoteOnly уточняет, что при этом строкой является только группа символов, заключенная в двойные кавычки.
hb_TokenCount() возвращает количество элементов в строке согласно изложенным выше правилам.
cToken := hb_TokenGet( cString, nToken, [cDelim], [lSkipStrings], [lDoubleQuoteOnly])Разбивает строку и возвращает элемент по номеру nToken в соответствии с правилами, изложенными в описании hb_TokenCount().
cToken := hb_TokenPtr( cString, @nSkip, [cDelim], [lSkipStrings], [lDoubleQuoteOnly])Работает как и hb_TokenGet(), но позволяет последовательно перебирать элементы, не производя разбиение каждый раз заново. Второй параметр nSkip, передаваемый по ссылке, указывает, сколько символов в строке надо пропустить, функция записывает в него новое значение, которое можно использовать при следующем ее вызове.
aTokens := hb_ATokens( cString, [cDelim], [lSkipStrings], [lDoubleQuoteOnly])Разбивает строку и возвращает массив со всеми элементами в соответствии с правилами, изложенными в описании hb_TokenCount().

3.13.5 Функции для работы с массивами

aArray := hb_ADel( aArray, nPos [,lChangeSize] ) Расширение стандартной функции ADel(). Удаляет заданный элемент массива, и, если lChangeSize установлен в .T., еще и уменьшает на 1 его размер - т.е., может использоваться вместо пары ADel(), ASize().
aArray := hb_AIns( aArray, nPos [,xItem] [,lChangeSize] ) Расширение стандартной функции AIns(). Вставляет xItem в заданную позицию массива, и, если lChangeSize установлен в .T., еще и увеличивает на 1 его размер - т.е., может использоваться вместо пары AIns(), ASize().
nResult := hb_Ascan( aArray, [block], [nStart], [nCount], [lExact] ) Расширение стандартной функции Ascan() - делает то же самое, но, если lExact установлен в .T., осуществляет точное сравнение.
nResult := hb_RAscan( aArray, [block], [nStart], [nCount], [lExact] ) Аналогична hb_Ascan(), но начинает поиск с конца массива.

3.13.6 Управление запуском процессов

nResult := hb_ProcessRun( cCommand, [cStdIn], [@cStdOut], [@cStdErr], [lDetach] )  
handle := hb_ProcessOpen( cCommand, [cStdIn], [@cStdOut], [@cStdErr], [lDetach] )  
nResult := hb_ProcessValue( handle, [lWait] )  
nResult := hb_ProcessClose( handle, lGentle )  

3.13.7 Битовые операции

z := hb_BitAnd( x, y ) Возвращает результат битовой операции И x & y
z := hb_BitOr( x, y ) Возвращает результат битовой операции ИЛИ x | y
z := hb_BitXor( x, y ) Возвращает результат битовой операции Исключающее ИЛИ x ^^ y
y := hb_BitNot( x ) Возвращает результат битовой операции НЕ x
y := hb_BitTest( x, n ) Проверяет, установлен ли в 1 n-й ( начиная с 0 ) ,бит числа x
y := hb_BitSet( x, n ) Возвращает число с установленным в 1 n-м ( начиная с 0 ) битом числа x
y := hb_BitReset( x, n ) Возвращает число со сброшенным в 0 n-м ( начиная с 0 ) битом числа x
z := hb_BitShift( x, y ) Битовый сдвиг x << y, если надо x >> y, то y должен быть отрицательным
y := hb_BitSwapI( x ) Возвращает результат перестановки байтов в 16-разрядном числе x
y := hb_BitSwapW( x ) Возвращает результат перестановки байтов в 16-разрядном unsigned числе x
y := hb_BitSwapL( x ) Возвращает результат перестановки байтов в 32-разрядном числе x
y := hb_BitSwapU( x ) Возвращает результат перестановки байтов в 32-разрядном unsigned числе x

3.13.8 Функции для манипуляции переменными

__mvPublic( cVarName ) Создает Public переменную с именем cVarName
__mvPrivate( cVarName ) Создает Private переменную с именем cVarName
nScope := __mvScope( cVarName ) Возвращает scope (диапазон действия, класс) переменной (константы определены в hbmemvar.ch):
         HB_MV_NOT_FOUND      = переменная не найдена
         HB_MV_UNKNOWN        = переменная не существует, но есть в symbol table
         HB_MV_ERROR          = информация недоступна ( ошибка памяти или аргумента )
         HB_MV_PUBLIC         = Public переменная
         HB_MV_PRIVATE_GLOBAL = Private переменная, определена за пределами текущей функции/процедуры
         HB_MV_PRIVATE_LOCAL  = Private переменная, определена в текущей функции/процедуре
      
__mvClear() Освобождает все Public и Private переменные, реализует команду CLEAR MEMORY
__mvDbgInfo( nScope [,nPosition [,@cVarName]] ) Возвращает информацию о переменных, соответствующих диапазону nScope ( HB_MV_PUBLIC,... - см.описание __mvScope() ) для отладчика; nPosition - позиция запрашиваемой переменной в диапазоне nScope, cVarName передается по ссылке, в нее записывается имя переменной, если задан nPosition. Возвращаемое функцией значение зависит от передаваемых параметров. Если передан только первый параметр, она возвращает количество переменных заданного nScope, если и второй ( nPosition ), то - значение соответствующей переменной. Если запрашиваемой переменной не существует (nPosition) больше количества переменных, то возвращается Nil, а в cVarName записывается "?".
lVarExist := __mvExist( cVarName ) Возвращает .T., если переменная cVarName ( Private или Public ) существует
xVal := __mvGet( cVarName ) Возвращает значение переменной cVarName ( Private или Public )
xVal := __mvPut( cVarName, xVal ) Устанавливает значение xVal переменной cVarName ( Private или Public )
lIsByRef := hb_IsByRef( @cVarName ) Проверяет, передана ли cVarName по ссылке, в функцию hb_IsByRef() она должна передаваться по ссылке.
__mvSetBase() Это, как называет ее разработчик, hacking function, ее можно использовать очень осторожно и с полным пониманием того, что вы делаете, т.к. она ломает обычное, правильное поведение программы - изменяет базовое смещение списка Private переменных. Вызов этой функции приводит к тому, что Private переменные, созданные в текущей функции, не освобождаются по выходу из нее, а наследуются функцией, из которой текущая была вызвана.

3.13.9 Функции сравнения по маске

lResult := hb_wildMatch( cPattern, cString, [lExact] ) Функция сравнивает строку cString с образцом cPattern и возвращает результат - истину или ложь. Образец может содержать символы "?" и "*" ( как известно, на месте "?" в cString может быть один любой символ, на месте "*" - несколько любых символов ). Если lExact - .T., строка целиком должна соответствовать образцу, если - .F. (по умолчанию), то хотя бы ее начало.
lResult := hb_wildMatchI( cPattern, cString ) То же, что и hb_wildMatch(), но она нечувствительна к регистру и не имеет третьего параметра - требует точного сравнения.
hb_filematch( cFile, cPattern )  

3.13.10 Функции для упаковки/распаковки строк, использующие zlib

cVersion := hb_zlibVersion() Возвращает строку с номером версии zlib, использующейся в Harbour.
nBytes := hb_zCompressBound( cData | nDataLen ) Возвращает максимальную длину упакованной строки, функции передается сама строка cData, которую следует упаковать, или ее длина.
nBytes := hb_zunCompressLen( cPackedData, [@nResult] ) Возвращает длину распакованной строки, функции передается упакованная строка cPackedData. Если передать по ссылке nResult, туда запишется результат операции ( 0 - все ОК ). Если возникла ошибка, то вместо длины распакованной строки возвращается -1.
cPackedData := hb_zCompress( cData, [@cBuffer | nBufLen], [@nResult], [nLevel] ) Возвращает упакованную строку, функции передается сама строка cData, которую следует упаковать, может также передаваться по ссылке буфер для упаковки (необходимый его размер может быть вычислен предварительно при помощи hb_zCompressBound(), уровень компрессии nLevel, который должен быть между 0 и 9 ( 0 - простое копирование без компрессии, 1 - наилучшая скорость, 9 - наилучшая компрессия. Если передать по ссылке nResult, туда запишется результат операции ( 0 - все ОК ). В случае ошибки функция возвращает Nil.
cUnPackedData := hb_zunCompress( cPackedData, [@cBuffer | nBufLen], [@nResult] ) Возвращает распакованную строку, функции передается сама строка cPackedData, которую следует распаковать, может также передаваться по ссылке буфер для распаковки (необходимый его размер может быть вычислен предварительно при помощи hb_zunCompressLen()). Если передать по ссылке nResult, туда запишется результат операции ( 0 - все ОК ). В случае ошибки функция возвращает Nil.
cError := hb_zError( nError ) Возвращает строку с описанием ошибки, возникшей при упаковке или распаковке, nError - код, который был записан в nResult при исполнении hb_zCompress()б hb_zunCompress() или hb_zunCompressLen().

3.13.11 Idle state

Idle state - это состояние ожидания, когда программа на Harbour ждет ввода от пользователя, с клавиатуры или мыши. Оно возникает как результат вызова функции Inkey() ( команда READ использует Inkey(), так что и при ожидании ввода GET-объектов возникает idle state ). В этом состоянии происходит автоматическая сборка мусора и могут выполняться какие-либо фоновые задачи. Для добавления/удаления этих фоновых задач и предназначены последующие функции (пример использования см. в harbour/tests/testidle.prg):

nHandle := hb_IdleAdd( bAction ) Кодоблок bAction добавляется в список фоновых задач для исполнения во время idle state. Функция возвращает nHandle, который может быть использован для удаления задачи из списка.
bAction := hb_IdleDel( nHandle ) Задача с хэндлом nHandle удаляется из списка фоновых задач, nHandle - значение, возвращенное функцией hb_IdleAdd() при добавлении задачи. Функция возвращает соответствующий задаче кодоблок, или Nil - если такой задачи нет в списке.
hb_IdleState() В результате выполнения этой функции программа входит в idle state, выполняет сборку мусора и выполняет одну из фоновых задач списка. В результате последовательного вызова этой функции фоновые задачи выполняются по очереди. Эту функцию имеет смысл вызывать, если у вас идет длительный процесс без состояний ожидания и вам надо прерывать его для выполнения фоновых задач.
hb_IdleSleep( nSeconds ) В результате выполнения этой функции программа входит в idle state на nSeconds секунд. В отличие от Inkey(nSeconds) состояние ожидания не прерывается при вводе с клавиатуры.

3.13.12 JSON - туда и обратно

cJSON := hb_jsonEncode( xValue [, lHuman] ) Возвращает JSON строку, полученную преобразованием xValue, lHuman ( по умолчанию - .F. ), если его значение - .T., делает строку более "читабельной".
Кодоблоки записываются в строку как "null", объекты - как массивы данных ( методы не записываются ).
nLengthDecoded := hb_jsonDecode( cJSON, @xValue ) Осуществляет обратное преобразование - из JSON строки cJSON в переменную xValue, передаваемую по ссылке. Возвращает количество обработанных символов строки.

3.13.13 Шифрование

Blowfish
cBfKey := hb_blowfishKey( cPasswd )Генерирует из пароля cPasswd и возвращает ключ cBfKey для последующего использования в операциях шифрации/дешифрации
cCipher := hb_blowfishEncrypt( cBfKey, cText [, lRaw ] )
cText := hb_blowfishDecrypt( cBfKey, cCipher [, lRaw ] )
cCipher := hb_blowfishEncrypt_CFB( cBfKey, cText [, cInitSeed ] )
cText := hb_blowfishDecrypt_CFB( cBfKey, cCipher [, cInitSeed ] )
MD5 - шифрование и хэширование
cCipher := hb_MD5Encrypt( cText, cPasswd )
cText := hb_MD5Decrypt( cCipher, cPasswd )
cDigest := hb_MD5( cText )
cDigest := hb_MD5File( cFilename )
SHA1, SHA2
cDigest := hb_Sha1( cText [, lRaw ] )
cDigest := hb_HMAC_Sha1( cText, cKey [, lRaw ] )
cDigest := hb_HMAC_SHA224( cText, cKey [, lRaw ] )
cDigest := hb_HMAC_SHA256( cText, cKey [, lRaw ] )
cDigest := hb_HMAC_SHA384( cText, cKey [, lRaw ] )
cDigest := hb_HMAC_SHA512( cText, cKey [, lRaw ] )

3.13.14 Разные функции - не знаю, в какую группу их включить

xResult := hb_ExecFromArray( cFuncName [,aParams] ) Возвращает результат исполнения функции, заданной по имени cFuncName с параметрами, переданными в массиве aParams, например:
           ? hb_execFromArray( "Str", {11,6,3} )
        
xResult := hb_ExecFromArray( @funcName [,aParams] ) Возвращает результат исполнения функции, заданной с помощью указателя @funcName с параметрами, переданными в массиве aParams, например:
           hfunc := @Str()
           ? hb_execFromArray( hfunc, {11,6,3} )
        
xResult := hb_ExecFromArray( bCodeBlock [,aParams] ) Возвращает результат исполнения кодоблока bCodeBlock с параметрами, переданными в массиве aParams:
           ? hb_execFromArray( {|n1,n2,n3|Str(n1,n2,n3)}, { 11,6,3 } )
        
xResult := hb_ExecFromArray( oObject, cMethodName [,aParams] ) Возвращает результат исполнения метода cMethodName объекта oObject с параметрами, переданными в массиве aParams, метод задан по имени.
xResult := hb_ExecFromArray( oObject, @msgName [,aParams] ) Возвращает результат исполнения метода объекта oObject с параметрами, переданными в массиве aParams, метод задан с помощью указателя @msgName
xResult := hb_ExecFromArray( aExecArray )В этом варианте использования hb_ExecFromArray() параметры передаются в виде массива:
           ? hb_execFromArray( { "Str", 11,6,3 } )
           ? hb_execFromArray( { hfunc, 11,6,3 } )
           ? hb_execFromArray( { {|n1,n2,n3|Str(n1,n2,n3)},11,6,3 } )
           и т.д.
        
hb_ForNext( nStart, nEnd | bEnd, bCode [, nStep ] ) Реализация цикла FOR ... NEXT в виде функции. Кодоблок bCode, которому передается счетчик ({|nIndex|...code...}), выполняется многократно при инкременте счетчика от nStart до nEnd (или до того, как кодоблок bEnd вернет .T.) с шагом nStep.
cDirBase := hb_DirBase() Возвращает базовый каталог - тот, где находится исполняемый файл.
cExePath := hb_Progname() Возвращает путь и имя исполняемого файла.
cSep := hb_ps() Возвращает символ, разделяющий каталоги в пути, принятый в ОС ( "/", "\")
cSep := hb_osDriveSeparator()  
cDirName := hb_DirTemp() Возвращает путь к системному временному каталогу.
lResult := hb_DirRemoveAll( cDir )  
n := hb_Rand32() Возвращает случайное целое число в диапазоне от 0 до 0xFFFFFFFF.
n := hb_RandomInt( [n1,] [n2] ) Возвращает случайное целое число. Необязательные параметры n1, n2 - диапазон; если n2 не задан, то возвращаемое значение - из диапазона между 0 и n1; если же оба параметра не заданы, то - из диапазона 0,1.
n := hb_Random( [n1,] [n2] ) Возвращает случайное вещественное число. Необязательные параметры n1, n2 - диапазон; если n2 не задан, то возвращаемое значение - из диапазона между 0 и n1; если же оба параметра не заданы, то - из диапазона между 0 и 1.
hb_RandomSeed( n ) Инициирует генерацию новой последовательности случайных чисел. Если параметр n равен 0, используется текущее время в миллисекундах и HVM stack address (в мультипоточном режиме), если же это другое число, то используется именно оно - на случай, если вам нужны повторяющиеся серии, возвращаемые hb_Random() и hb_RandomInt().