everscale-client-ruby 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,339 @@
1
+ require 'byebug'
2
+ require 'json'
3
+ require File.dirname(__FILE__) + '/helpers.rb'
4
+
5
+ class CodeGenerator
6
+ include InstanceHelpers
7
+
8
+ attr_accessor :types
9
+ attr_accessor :root_dir
10
+
11
+ TAB = " "
12
+
13
+ def initialize(types, root_dir)
14
+ @types = types
15
+ @root_dir = root_dir
16
+ end
17
+
18
+ def generate_self_code
19
+ generateModules(types)
20
+ generateReadme(types)
21
+ end
22
+
23
+ private
24
+
25
+ def generateModules(types)
26
+ types.modules.each do |mod|
27
+ modulesFolder = root_dir + "/lib/everscale-client-ruby/Client"
28
+ moduleFilePath = "#{modulesFolder}/#{mod.name.capitalize}.rb"
29
+ newModuleContent = ""
30
+ if mod.name == "client"
31
+ newModuleContent = generateClientModule(mod, types.modules)
32
+ else
33
+ newModuleContent = generateModule(mod)
34
+ end
35
+ if File.exists?(moduleFilePath)
36
+ File.delete(moduleFilePath)
37
+ end
38
+ File.open(moduleFilePath, 'w+') { |f| f.write(newModuleContent) }
39
+ end
40
+
41
+ p 'generate_self_code ok'
42
+ end
43
+
44
+ def generateReadme(types)
45
+ readmePath = root_dir + "/README.md"
46
+ content = %{
47
+ # Ruby Client for Free TON SDK
48
+
49
+ [![GEM](https://img.shields.io/badge/ruby-gem-orange)](https://rubygems.org/gems/everscale-client-ruby)
50
+ [![SPM](https://img.shields.io/badge/SDK%20VERSION-#{types.version}-green)](https://github.com/tonlabs/TON-SDK)
51
+
52
+ ## Install
53
+
54
+ Install gem
55
+ ```bash
56
+ gem install ton-client-ruby
57
+ ```
58
+
59
+ Install TON-SDK
60
+ ```bash
61
+ ton-client-ruby setup
62
+ # result - path to dylib file for ton-client-ruby configuration
63
+ ```
64
+
65
+ ### Manual build FreeTON SDK
66
+ 0. Install Rust to your OS
67
+ 1. git clone https://github.com/tonlabs/TON-SDK
68
+ 2. cd ./TON-SDK
69
+ 3. cargo update
70
+ 4. cargo build --release
71
+
72
+ ## Usage
73
+
74
+ ```ruby
75
+ # For MAcOS
76
+ TonClient.configure { |config| config.ffi_lib(./TON-SDK/target/release/libton_client.dylib) }
77
+ # For Linux
78
+ # TonClient.configure { |config| config.ffi_lib(./TON-SDK/target/release/libton_client.so) }
79
+
80
+ client = TonClient.create(config: {network: {server_address: "net.ton.dev"}})
81
+
82
+ # All methods are asynchronous
83
+
84
+ # example: call method for Crypto module
85
+ payload = {composite: '17ED48941A08F981'}
86
+ client.crypto.factorize(payload) do |response|
87
+ p response.result['factors']
88
+ end
89
+
90
+ # e.g. ...
91
+ ```\n\n
92
+ }
93
+ content << "## All Modules, methods and types\n\n"
94
+
95
+ # types
96
+ content << "<details>\n#{TAB}<summary>Types</summary>\n\n"
97
+
98
+ content << "#{customTypes()}\n"
99
+
100
+ types.modules.each do |mod|
101
+ (mod.enums || []).each do |type|
102
+ content << checkComment(type.summary, 0) if type.summary
103
+ content << checkComment(type.description, 0) if type.description
104
+ content << "\n- #### #{type.name}\n"
105
+ (type.cases || []).each do |enum_case|
106
+ content << "#{checkComment(enum_case.summary, 1).gsub(/#/, '')}" if enum_case.summary
107
+ content << "#{checkComment(enum_case.description, 1).gsub(/#/, '')}" if enum_case.description
108
+ content << "#{TAB}- case #{enum_case.name} = #{enum_case.value}\n\n"
109
+ end
110
+ end
111
+
112
+ (mod.types || []).each do |type|
113
+ content << checkComment(type.summary, 0) if type.summary
114
+ content << checkComment(type.description, 0) if type.description
115
+ content << "\n- #### #{type.name}\n"
116
+ (type.fields || []).each do |field|
117
+ content << "#{checkComment(field.summary, 1).gsub(/#/, '')}\n" if field.summary
118
+ content << "#{checkComment(field.description, 1).gsub(/#/, '')}\n" if field.description
119
+ content << "#{TAB}- #{field.name}: #{field.type}\n\n"
120
+ end
121
+ end
122
+ end
123
+ content << "</details>\n\n"
124
+
125
+ # methods
126
+ types.modules.each do |mod|
127
+ content << "<details>\n#{TAB}<summary>#{mod.name&.upcase}</summary>\n\n"
128
+ (mod.functions || []).each do |function|
129
+ content << "```ruby\n"
130
+ content << checkComment(function.summary, 2) if function.summary
131
+ content << checkComment(function.description, 2) if function.description
132
+ content << "\n#{TAB}#{TAB}def #{function.name}"
133
+ if function.arguments.empty?
134
+ content << "(&block)\n"
135
+ else
136
+ content << "(payload, &block)\n"
137
+ end
138
+ content << getFunctionComment(function, types)
139
+ content << "```\n"
140
+ end
141
+ content << "</details>\n\n"
142
+ end
143
+
144
+ content << %{
145
+ \n## Tests
146
+
147
+ 1. create __.env.test__ file inside root directory of this library with variables
148
+
149
+ example for NodeSE
150
+ ```
151
+ spec_ffi=./TON-SDK/target/release/libton_client.dylib
152
+ server_address=http://localhost:80
153
+ giver_abi_name=GiverNodeSE
154
+ giver_amount=10000000000
155
+ ```
156
+ 2. Run tests: inside folder of this library execute this commands
157
+ **rspec spec/binding.rb**
158
+ **rspec spec/client.rb**
159
+ **rspec spec/context.rb**
160
+ **rspec spec/abi.rb**
161
+ **rspec spec/boc.rb**
162
+ **rspec spec/crypto.rb**
163
+ **rspec spec/net.rb**
164
+ **rspec spec/processing.rb**
165
+ **rspec spec/tvm.rb**
166
+ **rspec spec/utils.rb**
167
+
168
+ \n## Update\n\n
169
+ ```\n
170
+ ton-client-ruby update\n
171
+ ```\n\n
172
+ or\n\n
173
+ ```\n
174
+ curl https://raw.githubusercontent.com/tonlabs/TON-SDK/master/tools/api.json > api.json\n\n
175
+ ```\n\n
176
+ ```\n
177
+ ton-client-ruby update ./api.json\n
178
+ ```\n
179
+ or\n\n
180
+ ```\n
181
+ cd ton-client-ruby\n
182
+ ```\n\n
183
+ ```\n
184
+ ./bin/ton-client-ruby update\n
185
+ ```\n
186
+ }
187
+ content = checkContent(content)
188
+ if File.exists?(readmePath)
189
+ File.delete(readmePath)
190
+ end
191
+ File.open(readmePath, 'w+') { |f| f.write(content) }
192
+ end
193
+
194
+ private def checkContent(content)
195
+ content.gsub!(/^([\s]+)# RESPONSE/, "\n\\1# RESPONSE")
196
+ content.gsub(/<Optional>/i, '&lt;Optional&gt;')
197
+ end
198
+
199
+ def customTypes
200
+ content = %{
201
+ - #### #{lib_prefix}MnemonicDictionary
202
+ #{TAB}- case TON = 0
203
+ #{TAB}- case ENGLISH = 1
204
+ #{TAB}- case CHINESE_SIMPLIFIED = 2
205
+ #{TAB}- case CHINESE_TRADITIONAL = 3
206
+ #{TAB}- case FRENCH = 4
207
+ #{TAB}- case ITALIAN = 5
208
+ #{TAB}- case JAPANESE = 6
209
+ #{TAB}- case KOREAN = 7
210
+ #{TAB}- case SPANISH = 8
211
+ }
212
+ content
213
+ end
214
+
215
+ private def checkComment(string, tabs = 1)
216
+ replacedString = "\n"
217
+ tab = ""
218
+ tabs.times do |i|
219
+ tab << TAB
220
+ end
221
+ comment = "#"
222
+ replacedString << "#{tab}#{comment} "
223
+ symbolsWithSpace = "\\.|\\:|\\!|\\?|\\;"
224
+ regxp = /([^#{symbolsWithSpace}])\n/
225
+ result = string
226
+ result.gsub!(/\n+/, "\n")
227
+ result.gsub!(/ \n/, "\n#{comment} ")
228
+ result.gsub!(/(#{symbolsWithSpace})\s*\n/, "\\1#{replacedString}")
229
+ result.gsub!(regxp, "\\1")
230
+ "#{tab}# #{result}"
231
+ end
232
+
233
+ private def generateClientModule(mod, modules)
234
+ content = "module TonClient\n\n#{TAB}class #{mod.name.capitalize}\n#{TAB}#{TAB}include CommonInstanceHelpers\n\n"
235
+ content << "#{TAB}#{TAB}attr_reader :core, :context\n"
236
+ content << "#{TAB}#{TAB}private_accessor "
237
+ modules.each_with_index do |m, i|
238
+ next if m.name.downcase == 'client'
239
+ content << ((modules.size - 1) == i ? ":_#{m.name}\n" : ":_#{m.name}, ")
240
+ end
241
+ content << "#{TAB}#{TAB}MODULE = self.to_s.downcase.gsub(/^(.+::|)(\\w+)$/, '\\2').freeze\n\n"
242
+ content << "#{TAB}#{TAB}def initialize(context: Context.new, core: TonClient::TonBinding)\n"
243
+ content << "#{TAB}#{TAB}#{TAB}@context = context\n"
244
+ content << "#{TAB}#{TAB}#{TAB}@core = core\n#{TAB}#{TAB}end\n\n"
245
+ content << "#{TAB}#{TAB}def destroy_context\n"
246
+ content << "#{TAB}#{TAB}#{TAB}core.tc_destroy_context(context.id)\n#{TAB}#{TAB}end\n\n"
247
+ modules.each_with_index do |m, i|
248
+ next if m.name.downcase == 'client'
249
+ content << "#{TAB}#{TAB}def #{m.name}\n"
250
+ content << "#{TAB}#{TAB}#{TAB}_#{m.name} ||= #{m.name.capitalize}.new(context: context)\n"
251
+ content << "#{TAB}#{TAB}end\n\n"
252
+ end
253
+
254
+ mod.functions.each do |func|
255
+ content << gen_function(func, types)
256
+ end
257
+
258
+ content << "#{TAB}end\n"
259
+ content << "end\n\n"
260
+
261
+ content
262
+ end
263
+
264
+ private def generateModule(mod)
265
+ content = "module TonClient\n\n#{TAB}class #{mod.name.capitalize}\n#{TAB}#{TAB}include CommonInstanceHelpers\n\n"
266
+ content << "#{TAB}#{TAB}attr_reader :core, :context\n"
267
+ content << "#{TAB}#{TAB}MODULE = self.to_s.downcase.gsub(/^(.+::|)(\\w+)$/, '\\2').freeze\n\n"
268
+ content << "#{TAB}#{TAB}def initialize(context: Context.new, core: TonClient::TonBinding)\n"
269
+ content << "#{TAB}#{TAB}#{TAB}@context = context\n"
270
+ content << "#{TAB}#{TAB}#{TAB}@core = core\n"
271
+ content << "#{TAB}#{TAB}end\n\n"
272
+
273
+ mod.functions.each do |func|
274
+ content << gen_function(func, types)
275
+ end
276
+
277
+ content << "#{TAB}end\n"
278
+ content << "end\n\n"
279
+
280
+ content
281
+ end
282
+
283
+ private def gen_function(function, types)
284
+ content = getFunctionComment(function, types)
285
+ content << "#{TAB}#{TAB}def #{function.name}"
286
+ if function.arguments.empty?
287
+ content << "(&block)\n"
288
+ content << "#{TAB}#{TAB}#{TAB}core.requestLibrary(context: context.id, method_name: full_method_name(MODULE, __method__.to_s), payload: {}, &block)\n"
289
+ else
290
+ content << "(payload, &block)\n"
291
+ content << "#{TAB}#{TAB}#{TAB}core.requestLibrary(context: context.id, method_name: full_method_name(MODULE, __method__.to_s), payload: payload, &block)\n"
292
+ end
293
+ content << "#{TAB}#{TAB}end\n\n"
294
+
295
+ content
296
+ end
297
+
298
+ def getFunctionComment(function, types)
299
+ content = ''
300
+ if argument = function.arguments.first
301
+ content << "#{TAB}#{TAB}# INPUT: #{argument.type}\n"
302
+ if types.all_types[argument.type].respond_to?(:fields)
303
+ types.all_types[argument.type].fields.each do |arg|
304
+ content << "#{TAB}#{TAB}# #{arg.name}: #{arg.type} - "
305
+ content << "#{TAB}#{TAB}# #{checkComment(arg.summary, 2)}" if arg.summary
306
+ content << "#{TAB}#{TAB}# #{checkComment(arg.description, 2)}" if arg.description
307
+ content << "\n"
308
+ end
309
+ elsif types.all_types[argument.type].respond_to?(:cases)
310
+ end
311
+ end
312
+
313
+ if types.all_types[function.result]
314
+ content << "#{TAB}#{TAB}# RESPONSE: #{function.result}\n"
315
+ if types.all_types[function.result].respond_to?(:fields)
316
+ types.all_types[function.result].fields.each do |arg|
317
+ content << "#{TAB}#{TAB}# #{arg.name}: #{arg.type} - "
318
+ content << "#{TAB}#{TAB}# #{checkComment(arg.summary, 2)}" if arg.summary
319
+ content << "#{TAB}#{TAB}# #{checkComment(arg.description, 2)}" if arg.description
320
+ content << "\n"
321
+ end
322
+ elsif types.all_types[function.result].respond_to?(:cases)
323
+ end
324
+ end
325
+
326
+ content
327
+ end
328
+ end
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
338
+
339
+
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+ require 'byebug'
3
+
4
+ module InstanceHelpers
5
+
6
+ def lib_prefix
7
+ ''
8
+ end
9
+
10
+ def lib_enum_postfix
11
+ ''
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ script_file_path = File.expand_path(File.dirname(__FILE__))
4
+
5
+ require "#{script_file_path}/api_converter.rb"
6
+ require "#{script_file_path}/code_generator.rb"
7
+ GEM_DIR = "#{script_file_path}/../.."
8
+
9
+
10
+ `cd #{script_file_path} && curl https://raw.githubusercontent.com/tonlabs/TON-SDK/master/tools/api.json > api.json`
11
+ api_json_path = "#{script_file_path}/../../api.json"
12
+ json = ''
13
+ if File.exists?(api_json_path)
14
+ json = File.read(api_json_path)
15
+ else
16
+ p "File #{api_json_path} is not exist"
17
+ exit 0
18
+ end
19
+ converter = ApiConverter.new(json)
20
+ types = converter.convert
21
+ generator = CodeGenerator.new(types, GEM_DIR)
22
+ generator.generate_self_code
23
+
24
+ version_file = "#{script_file_path}/../everscale-client-ruby/version.rb"
25
+ file = File.read(version_file)
26
+
27
+ p 'check version'
28
+ if file[/VERSION = "(\d+)\.(\d+)\.(\d+)"/]
29
+ major = $1
30
+ minor = $2
31
+ current = $3
32
+ version = "#{major}.#{minor}.#{current.to_i + 1}"
33
+ p version
34
+ data = "module TonClient\n VERSION = \"#{version}\"\nend\n\n"
35
+ p data
36
+ p version_file
37
+ File.open(version_file, 'wb') { |f| f.write(data) }
38
+ p 'update version'
39
+
40
+ puts "make release? Y/N"
41
+ option = gets
42
+ if option.strip.downcase == 'y'
43
+ system(%{cd #{GEM_DIR} && git add .})
44
+ system(%{cd #{GEM_DIR} && git commit -m 'version #{version}'})
45
+ system(%{cd #{GEM_DIR} && bash -lc 'rake release'})
46
+ end
47
+ end
48
+
@@ -0,0 +1,209 @@
1
+ module TonClient
2
+ module TonBinding
3
+ @@request_id = 0
4
+ @@requests = {}
5
+
6
+ class Response
7
+ attr_reader :core
8
+ attr_accessor :result, :error, :custom_response, :finished, :request_id, :current_response
9
+
10
+ def initialize(core: TonClient::TonBinding)
11
+ @core = core
12
+ end
13
+
14
+ def update(request_id, string_data, response_type, finished)
15
+ response_hash = core.read_string_to_hash(string_data)
16
+ self.finished = finished
17
+ self.request_id = request_id
18
+ self.current_response = response_hash
19
+ case response_type
20
+ when 0
21
+ # result
22
+ self.result = response_hash
23
+ when 1
24
+ # error
25
+ self.error = response_hash
26
+ else
27
+ # another
28
+ if response_type >= 100
29
+ self.custom_responses = response_hash
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.generate_request_id
36
+ @@request_id = 0 if @@request_id == 4294967295
37
+ @@request_id += 1
38
+ end
39
+
40
+ def self.get_request(id)
41
+ @@requests[id]
42
+ end
43
+
44
+ def self.delete_request(id)
45
+ @@requests[id] = nil
46
+ end
47
+
48
+ def self.set_request(id, &request_block)
49
+ @@requests[id] = request_block
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ module TonClient
56
+
57
+ module TonBinding
58
+ extend FFI::Library
59
+ ffi_lib FFI::Library::LIBC
60
+
61
+ # memory allocators
62
+ attach_function :malloc, [:size_t], :pointer
63
+ attach_function :calloc, [:size_t], :pointer
64
+ attach_function :valloc, [:size_t], :pointer
65
+ attach_function :realloc, [:pointer, :size_t], :pointer
66
+ attach_function :free, [:pointer], :void
67
+
68
+ # memory movers
69
+ attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer
70
+ attach_function :bcopy, [:pointer, :pointer, :size_t], :void
71
+
72
+ def self.setup_bindings
73
+
74
+ # tc_string_handle_t* tc_create_context(tc_string_data_t config);
75
+ # attach_function :tc_create_context, [TcStringDataT.by_value], TcStringHandleT.by_ref
76
+ attach_function :tc_create_context, [TcStringDataT.by_value], :pointer
77
+
78
+ # fn tc_destroy_context(context: InteropContext)
79
+ attach_function :tc_destroy_context, [:uint32], :void
80
+
81
+ # tc_string_handle_t* tc_request_sync(
82
+ # uint32_t context,
83
+ # tc_string_data_t function_name,
84
+ # tc_string_data_t function_params_json);
85
+ # attach_function :tc_request_sync, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value], TcStringHandleT.by_ref
86
+ attach_function :tc_request_sync, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value], :pointer
87
+
88
+ # enum tc_response_types_t {
89
+ # tc_response_success = 0,
90
+ # tc_response_error = 1,
91
+ # tc_response_nop = 2,
92
+ # tc_response_custom = 100,
93
+ # };
94
+ # typedef void (*tc_response_handler_t)(
95
+ # uint32_t request_id,
96
+ # tc_string_data_t params_json,
97
+ # uint32_t response_type,
98
+ # bool finished);
99
+ callback :tc_response_handler_t, [:uint32, TcStringDataT.by_value, :uint32, :bool], :void
100
+
101
+ # void tc_request(
102
+ # uint32_t context,
103
+ # tc_string_data_t function_name,
104
+ # tc_string_data_t function_params_json,
105
+ # uint32_t request_id,
106
+ # tc_response_handler_t response_handler);
107
+ attach_function :tc_request, [:uint32, TcStringDataT.by_value, TcStringDataT.by_value, :uint32, :tc_response_handler_t], :void
108
+
109
+ # tc_string_data_t tc_read_string(const tc_string_handle_t* handle);
110
+ # attach_function :tc_read_string, [TcStringHandleT.by_ref], TcStringDataT.by_value
111
+ attach_function :tc_read_string, [:pointer], TcStringDataT.by_value
112
+
113
+ # void tc_destroy_string(const tc_string_handle_t* handle)
114
+ # attach_function :tc_destroy_string, [TcStringHandleT.by_ref], :void
115
+ attach_function :tc_destroy_string, [:pointer], :void
116
+ end
117
+
118
+ def self.make_string(string)
119
+ result = TonBinding::TcStringDataT.new
120
+ result[:content] = FFI::MemoryPointer.from_string(string)
121
+ result[:len] = string.bytesize
122
+ result
123
+ end
124
+
125
+ # def self.read_string(tc_string_handle)
126
+ # is_ref = tc_string_handle.class == TonClient::TonBinding::TcStringHandleT
127
+ # if is_ref
128
+ # string = tc_read_string(tc_string_handle)
129
+ # else
130
+ # string = tc_string_handle
131
+ # end
132
+
133
+ # if string[:content].address > 1
134
+ # string = string[:content].read_string(string[:len]).force_encoding('UTF-8') + ''
135
+ # tc_destroy_string(tc_string_handle) if is_ref
136
+ # return string
137
+ # end
138
+ # nil
139
+ # end
140
+
141
+ def self.read_string(tc_string_handle)
142
+ is_ref = tc_string_handle.class == FFI::Pointer
143
+ if is_ref
144
+ string = tc_read_string(tc_string_handle)
145
+ else
146
+ string = tc_string_handle
147
+ end
148
+
149
+ if string[:content].address > 1
150
+ string = string[:content].read_string(string[:len]).force_encoding('UTF-8') + ''
151
+ tc_destroy_string(tc_string_handle) if is_ref
152
+ return string
153
+ end
154
+ nil
155
+ end
156
+
157
+ def self.read_string_to_hash(tc_string_handle_t_ref)
158
+ json_string = read_string(tc_string_handle_t_ref)
159
+ JSON.parse(json_string, {max_nesting: false}) if json_string
160
+ end
161
+
162
+ def self.send_request_sync(context: 1, method_name: '', payload: {})
163
+ raise 'context not found' unless context
164
+ raise 'method_name is empty' if method_name.empty?
165
+
166
+ method_name_string = make_string(method_name)
167
+ payload_string = make_string(payload.to_json)
168
+
169
+ sdk_json_response = tc_request_sync(context, method_name_string, payload_string)
170
+ response = read_string_to_hash(sdk_json_response)
171
+
172
+ return response['result'] if response['result']
173
+ return response['error'] if response['error']
174
+ end
175
+
176
+ def self.send_request(context: 1, method_name: '', payload: {}, request_id: 1, &block)
177
+ raise 'context not found' unless context
178
+ raise 'method_name is empty' if method_name.empty?
179
+
180
+ if block
181
+ method_name_string = make_string(method_name)
182
+ payload_string = make_string(payload.to_json)
183
+ tc_request(context, method_name_string, payload_string, request_id, &block)
184
+ end
185
+ end
186
+
187
+ # block = { |response| }
188
+ def self.requestLibrary(context: 1, method_name: '', payload: {}, &block)
189
+ request_id = generate_request_id
190
+ set_request(request_id, &block)
191
+ send_request(context: context, method_name: method_name, payload: payload, request_id: request_id) do |request_id, string_data, response_type, finished|
192
+ if get_request(request_id)
193
+ response = Response.new
194
+ response.update(request_id, string_data, response_type, finished)
195
+ get_request(request_id).call(response)
196
+ delete_request(request_id) if finished
197
+ end
198
+ end
199
+ end
200
+
201
+ end
202
+ end
203
+
204
+
205
+
206
+
207
+
208
+
209
+
@@ -0,0 +1,21 @@
1
+ module TonClient
2
+ module TonBinding
3
+
4
+ # typedef struct {
5
+ # const char* content;
6
+ # uint32_t len;
7
+ # } tc_string_data_t;
8
+ class TcStringDataT < FFI::Struct
9
+ layout :content, :pointer,
10
+ :len, :uint32
11
+ end
12
+
13
+
14
+ # typedef struct {
15
+ # } tc_string_handle_t;
16
+ class TcStringHandleT < FFI::Struct
17
+ layout :content, :pointer,
18
+ :len, :uint32
19
+ end
20
+ end
21
+ end