Python语言与C语言数据交互的场景还是比较常见的,例如在使用python优秀的数据处理可视化等优势的同时,对于某些优秀的开源C/C++的软件库的调用就需要用到ctypes库函数对动态库进行API的灵活调用了,再例如在某些场景下,C语言的数据需要可视化,而C语言的可视化接口的支持是很薄弱的,这里可以采用Python强大的可视化效果来验证数据的正确性(也可以采用MATLAB完成可视化)。 再比如在某些Demo测试场景下,使用Python便捷部署的神经网络框架在使用获取内存图像数据时也可能使用到Python和C之间数据交换的需求。
一、python与C交互的重要库 ctypes
ctypes库作为python与C之间的交互的重要库,其定义了各类数据类型与C语言中的数据类型进行对应,其中包括了char,int,POINTER等等,具体可以参看数据手册。ctypes还能够通过CDLL接口应用C语言的动态库.so,在调用接口过程中,应该要严格配置Python端调用动态库函数接口的参数类型(不能有任何偏差),应该掌握如何定义数组缓冲区并操作地址变量,同时能够使用接口获取变量地址,从而传给C-API使用,基础如下所示:
1. 数组类型定义及使用
测试代码:
1 # 特殊ctypes类型数组类型定义 2 ArrayTestType = ctypes.c_uint8 * 10 # 重定义数据类型 3 ArrayTest = ArrayTestType(11) # 定义数据并初始化数据赋值为11 4 ArrayTest[5] = 20 # 对数据进行赋值处理 5 print('Line', sys._getframe().f_lineno, ':', ArrayTest, type(ArrayTest), ArrayTestType, type(ArrayTestType)) 6 print('Line', sys._getframe().f_lineno, ':', ArrayTest[0], ArrayTest[5])
运行结果:
Line 20 : <__main__.c_ubyte_Array_10 object at 0x7fd1318e8ea0> <class '__main__.c_ubyte_Array_10'> <class '__main__.c_ubyte_Array_10'> <class '_ctypes.PyCArrayType'> Line 21 : 11 20
这里可以看到 ArrayTestType 的类型其实为 PyCArrayType PythonC数组类型,因此 ArrayTestType 可以定义一个 unsigned char Array[10] 的数组。
注:数组类型的缩略定义方法(相当于上述步骤的两步)
# 定义一个大小为10指针实例作为缓存, 等效为 Step1:DataType = c_uint8 * LENGTH --> Step2:DataPoint = DataType() DataPoint = (c_uint8 * LENGTH)()
2. 动态库的加载及接口调用
测试代码:
1 # 从C语言或者C++的动态库当中加载C函数以调用 2 p = os.getcwd() + '/libfunc.so' # 获取当前的动态库的绝对路径位置 3 f = cdll.LoadLibrary(p) # 使用LoadLibrary接口加载C语言动态库 4 5 # Python Class类 到 C struct结构体 数据类型的传输转换 6 Sfunction = f.py_struct_address # 从当前库当中取得clib中的函数py_struct_address,并重命名为Sfunction 7 Sfunction.argtypes = [POINTER(POINT), POINTER(ctypes.c_char)] # 设置当前函数的输入参数
这里使用了 os模块获取了动态库的绝对路径,并调用cdll.LoadLibrary(libpath),这里使用 argtypes 定义了动态库函数的输入参数类型,分别为结构体指针、char *指针。
3. 定义字符串类型(字符串string)
1 p = create_string_buffer(b"Hello World", 15) # create a 10 byte buffer 2 print('Line', sys._getframe().f_lineno, ':', p,sizeof(p), repr(p.raw))
创建一个string类型的缓冲空间,并返回一个字符串指针指向这串字符串,create_string_buffer 参数分别为字符串、buffer总长度,这个长度不能小于前一个参数字符串的长度大小,否则会报错,p.raw取字符串的内容。
4. Numpy数组的地址获取及应用
测试代码:
1 # Numpy 数据类型等相互转换测试(将内存数据转换值Python当中) 2 ImgW = 1669 # 图像宽度 3 ImgH = 21 # 图像高度 4 ImgC = 3 # 图像通道数 5 ImgL = ImgW*ImgH*ImgC # 图像总长度 6 7 ImgArray = np.zeros((ImgW,ImgH,ImgC), dtype=np.ubyte) # 申请图像总空间为多维 zeros 矩阵 8 print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))) # 将numpy数组转换为地址表示方式 data_as 并打印数据类型 9 ImgArray_addr = ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) # 获取的指针放置在ImgArray_addr变量当中 10 print('Line', sys._getframe().f_lineno, ':', ImgArray_addr[0],ImgArray_addr[1],ImgArray_addr[2],ImgArray_addr[3]) 11 12 Imgfunction = f.py_img_address # 从当前库当中取得clib中的函数py_struct_address,并重命名为Sfunction 13 Imgfunction.argtypes = [POINTER(ctypes.c_ubyte), ctypes.c_uint32] # 设置当前函数的输入参数规划 usigned char *pointer, unsigned int arg 14 Imgfunction.restype = ctypes.c_int # 设置当前函数返回值参数为 int 类型 15 time_start = time.time() 16 Res = Imgfunction(ImgArray_addr, ImgL) # 调用Imgfunction函数 17 time_end = time.time() 18 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 记录当前函数调用消耗的时间 19 20 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,0]) # 打印0通道的数据 21 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,1]) # 打印1通道的数据 22 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,2]) # 答应2通道的数据
通过numpy库中数组的成员 ctypes.data_as 方法获取指定类型的指针地址,并将此地址应用在函数库的参数中。
5. 基本类型定义
测试代码:
1 # 使用byref获取ctypes类型数据的地址 2 data = ctypes.c_uint8(10) # 定义一个整数类型的变量,变量初始值为 10,相当于C语言中的 char data=42; 3 data_addr = ctypes.byref(data, 0) # 通过使用byref接口获取地址,相当于C语言中的 char *data_addr = &data; byref(obj, offset) 对应于这段 C 代码:(((char *)&obj) + offset) 4 print('Line', sys._getframe().f_lineno, ':', type(data)) 5 print('Line', sys._getframe().f_lineno, ':', type(data_addr))
这里使用 ctypes.c_uint8(10) 定义一个 unsigned char 类型的数据并赋予初值为10,byref函数接口用于获取数据data的地址。
I) 完整的测试代码:
MemoryTest.py
1 import os 2 import sys 3 import time 4 import ctypes 5 from ctypes import * 6 from ctypes import cdll 7 import numpy as np 8 9 BUFF_SIZE = 6*1024*1024 # 基本的缓冲区域尺寸大小定义 10 LENGTH = 2*2*3 # 数据长度定义 11 12 # 类定义 13 class POINT(Structure): # 定义了一个类,类当中的基本成员变量包括了x、y, 相当于C语言中的 struct POINT{int x;inty}; 14 _fields_ = [("x", c_int),("y", c_int),("addr", POINTER(c_uint8)),("len", c_int)] 15 16 # 特殊ctypes类型数组类型定义 17 ArrayTestType = ctypes.c_uint8 * 10 # 重定义数据类型 18 ArrayTest = ArrayTestType(11) # 定义数据并初始化数据赋值为11 19 ArrayTest[5] = 20 # 对数据进行赋值处理 20 print('Line', sys._getframe().f_lineno, ':', ArrayTest, type(ArrayTest), ArrayTestType, type(ArrayTestType)) 21 print('Line', sys._getframe().f_lineno, ':', ArrayTest[0], ArrayTest[5]) 22 23 DataPoint = (c_uint8 * LENGTH)() # 定义一个大小为10指针实例作为缓存, 等效为 Step1:DataType = c_uint8 * LENGTH --> Step2:DataPoint = DataType() 24 DataPoint[3] = 22 25 26 point = POINT(10, 20, DataPoint,LENGTH) # 定义个类对象Obj, 相当于 struct POINT point={10,20}; 27 print('Line', sys._getframe().f_lineno, ':', point.x, point.y, point.addr[3], point.len) 28 29 point = POINT(y=5) # 定义个类对象Obj, 相当于 struct POINT point={,20}; 30 print('Line', sys._getframe().f_lineno, ':', point.x, point.y) 31 32 # Class类数组定义 33 addrTest = (POINT * 1)() # 定义一个POINT结构体缓冲地址 34 addrTest[0].x = 10 # POINT缓冲地址的第一个变量的x成员值 35 addrTest[0].y = 11 # POINT缓冲地址的第一个变量的y成员值 36 addrTest[0].addr = DataPoint # POINT缓冲地址的第一个变量的addr成员赋值 37 addrTest[0].len = LENGTH # POINT缓冲地址的第一个变量的len成员赋值 38 print('Line', sys._getframe().f_lineno, ':', addrTest, addrTest[0].x,addrTest[0].y,addrTest[0].addr[3],addrTest[0].len) 39 40 TenPointsArrayType = POINT * 3 # 重定义了一个POINT数组类型,相当于C语言中的 #define TenPointsArrayType POINT*3 41 arr = TenPointsArrayType() 42 for pt in arr: 43 print(pt.x, pt.y, pt.addr) 44 45 # 从C语言或者C++的动态库当中加载C函数以调用 46 p = os.getcwd() + '/libfunc.so' # 获取当前的动态库的绝对路径位置 47 f = cdll.LoadLibrary(p) # 使用LoadLibrary接口加载C语言动态库 48 function = f.py_point_address # 从当前库当中取得clib中的函数py_point_address,并重命名为function 49 function.argtypes = [POINTER(c_byte)] # 设置当前函数的输入参数 50 51 # Python Class类 到 C struct结构体 数据类型的传输转换 52 Sfunction = f.py_struct_address # 从当前库当中取得clib中的函数py_struct_address,并重命名为Sfunction 53 Sfunction.argtypes = [POINTER(POINT), POINTER(ctypes.c_char)] # 设置当前函数的输入参数 54 55 p = create_string_buffer(b"Hello World", 15) # create a 10 byte buffer 56 print('Line', sys._getframe().f_lineno, ':', p,sizeof(p), repr(p.raw)) 57 58 time_start = time.time() 59 Res = Sfunction(addrTest, p) 60 time_end = time.time() 61 for i in range(LENGTH): 62 print(addrTest[0].addr[i]) 63 64 print('Line', sys._getframe().f_lineno, ':', addrTest[0].x, addrTest[0].y, addrTest[0].len, addrTest[0].addr) 65 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 记录当前函数调用消耗的时间 66 67 # Numpy 数据类型等相互转换测试(将内存数据转换值Python当中) 68 ImgW = 1669 # 图像宽度 69 ImgH = 21 # 图像高度 70 ImgC = 3 # 图像通道数 71 ImgL = ImgW*ImgH*ImgC # 图像总长度 72 # ImgArray = np.array([[0, 1], [2, 3]], dtype=np.uint8) 73 ImgArray = np.zeros((ImgW,ImgH,ImgC), dtype=np.ubyte) # 申请图像总空间为多维 zeros 矩阵 74 # ImgArray = np.array([1,2,3,4], dtype=np.int32) 75 # print(ImgArray) 76 # print(ImgArray.ctypes.data) 77 print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8))) # 将numpy数组转换为地址表示方式 data_as 并打印数据类型 78 ImgArray_addr = ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)) # 获取的指针放置在ImgArray_addr变量当中 79 print('Line', sys._getframe().f_lineno, ':', ImgArray_addr[0],ImgArray_addr[1],ImgArray_addr[2],ImgArray_addr[3]) 80 # print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)).contents) 81 # print(ImgArray.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)).contents) 82 # print(ImgArray.ctypes.shape) 83 # print(ImgArray.ctypes.strides) 84 85 Imgfunction = f.py_img_address # 从当前库当中取得clib中的函数py_struct_address,并重命名为Sfunction 86 Imgfunction.argtypes = [POINTER(ctypes.c_ubyte), ctypes.c_uint32] # 设置当前函数的输入参数规划 usigned char *pointer, unsigned int arg 87 Imgfunction.restype = ctypes.c_int # 设置当前函数返回值参数为 int 类型 88 time_start = time.time() 89 Res = Imgfunction(ImgArray_addr, ImgL) # 调用Imgfunction函数 90 time_end = time.time() 91 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 记录当前函数调用消耗的时间 92 93 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,0]) # 打印0通道的数据 94 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,1]) # 打印1通道的数据 95 print('Line', sys._getframe().f_lineno, ':/n', ImgArray[:,:,2]) # 答应2通道的数据 96 97 # 一般类型定义以及数据取地址方法 int *pi = &i; 98 i = ctypes.c_int(42) # 定义一个整数类型的变量,变量初始值为 42,相当于C语言中的 int i=42; 99 pi = ctypes.pointer(i) # 通过使用pointer接口获取,相当于C语言中的 int *pi = &i; 100 print('Line', sys._getframe().f_lineno, ':', pi.contents) # 查看指针变量的信息 101 print('Line', sys._getframe().f_lineno, ':', pi[0]) # 查看指针所指向的内容,相当于C语言中的 *pi; 102 103 # ctypes.c_byte 类型的数组定义,等效于 byte a[BUFF_SIZE]; 104 a = (c_byte * BUFF_SIZE)() # 定义一个大小为BUFF_SIZE指针实例作为缓存 105 # cast(a, POINTER(c_uint8)) # 函数可以将一个指针实例强制转换为另一种 ctypes 类型 106 print('Line', sys._getframe().f_lineno, ':', a) 107 print('Line', sys._getframe().f_lineno, ':', type(a)) 108 time_start = time.time() 109 function(a) # 执行function函数并传入a地址参数 110 time_end = time.time() 111 112 for i in range(10): 113 print(a[i]) 114 115 print('Line', sys._getframe().f_lineno, ':', 'TimeCost:', (time_end - time_start)*1000, 'ms') # 记录当前函数调用消耗的时间 116 117 # 使用byref获取ctypes类型数据的地址 118 data = ctypes.c_uint8(10) # 定义一个整数类型的变量,变量初始值为 10,相当于C语言中的 char data=42; 119 data_addr = ctypes.byref(data, 0) # 通过使用byref接口获取地址,相当于C语言中的 char *data_addr = &data; byref(obj, offset) 对应于这段 C 代码:(((char *)&obj) + offset) 120 print('Line', sys._getframe().f_lineno, ':', type(data)) 121 print('Line', sys._getframe().f_lineno, ':', type(data_addr)) 122 123 # 使用id获取变量在python的地址 124 value = 'hello world' # 定义一个字符串变量 125 address = id(value) # 获取value的地址,赋给address 126 get_value = ctypes.cast(address, ctypes.py_object).value # 读取地址中的变量 127 print('Line', sys._getframe().f_lineno, ':', address, get_value) # 128 129 # 一般Clib函数的调用 130 res = f.func(99) # 普通函数调用 131 print('Line', sys._getframe().f_lineno, ':', res)
View Code
function.c
1 #include <stdio.h> 2 #include <sys/shm.h> 3 #include <string.h> 4 #include <stdlib.h> 5 #include <time.h> 6 #include <sys/time.h> 7 8 9 #define BUFF_SIZE 6*1024*1024 10 11 typedef struct T_POINT{ 12 int x; 13 int y; 14 char * addr; 15 int len; 16 }POINT; 17 18 time_t get_timestamp_us(void) 19 { 20 time_t timestamp_ms = 0; 21 struct timeval tv; 22 23 gettimeofday(&tv,NULL); 24 timestamp_ms = tv.tv_sec * 1000 * 1000 + tv.tv_usec; 25 return timestamp_ms; 26 } 27 28 char *file_read(unsigned long *file_bytes, char *file_name) 29 { 30 int file_size; 31 FILE *fd = NULL; 32 char *file_data = NULL; 33 fd = fopen(file_name, "rw"); 34 if(fd < 0) 35 { 36 printf("File open failed.../n"); 37 return NULL; 38 } 39 fseek(fd, 0, SEEK_END); 40 file_size = ftell (fd); 41 file_data = malloc(sizeof(char)*file_size); 42 if(file_data == NULL) 43 { 44 printf("Malloc failed.../n"); 45 return NULL; 46 } 47 fseek(fd, 0, SEEK_SET); 48 *file_bytes = fread(file_data,sizeof(char),file_size,fd); 49 fclose(fd); 50 return file_data; 51 } 52 53 /* func.c */ 54 int func(int a) 55 { 56 return a*a; 57 } 58 59 void cycle_calc(int b) 60 { 61 int count = 100; 62 while(count--){ 63 b*=2; 64 printf("%d - %d/n", count, b); 65 } 66 } 67 68 unsigned char * c_point_address(void) 69 { 70 unsigned char *Img = malloc(sizeof(unsigned char)*1000); 71 printf("C-Address:%hhn/n", Img); 72 memset(Img, 20, 1000); 73 return Img; 74 } 75 76 int py_point_address(unsigned char * Addr) 77 { 78 unsigned char *Img = malloc(sizeof(unsigned char)*BUFF_SIZE); 79 // printf("C-Address:%x/n", Img); 80 // printf("Python-Address:%x/n", Addr); 81 memset(Img, 20, BUFF_SIZE); 82 memcpy((unsigned char * )Addr, Img, BUFF_SIZE); 83 return 1; 84 } 85 86 int py_struct_address(POINT *pt_POINT, char *str) 87 { 88 int i = 0; 89 for(i=0; i < pt_POINT->len; i++) 90 { 91 pt_POINT->addr[i] = i; 92 } 93 pt_POINT->x = 16; 94 pt_POINT->y = 17; 95 printf("FunctionPrint:%s/n",str); 96 return 1; 97 } 98 99 int py_img_address(unsigned char *data, unsigned int lenght) 100 { 101 time_t st,et; 102 unsigned long count=0; 103 unsigned long i=0; 104 st = get_timestamp_us(); 105 unsigned char *ImgData = file_read(&i, "./Test.PNG"); 106 et = get_timestamp_us(); 107 printf("C ### ReadFile time Cost:%ld/n", et - st); 108 109 printf("Data[%ld]:%d/n", count, *(ImgData+i-1)); 110 111 st = get_timestamp_us(); 112 memcpy(data, ImgData, sizeof(unsigned char)*lenght); 113 et = get_timestamp_us(); 114 printf("C ### Memcpy time Cost:%ld/n", et - st); 115 116 printf("py_img_address FunctionPrint:%ld/n", i); 117 return 1; 118 }
View Code
exe_shell.sh
1 #!/bin/bash 2 gcc -fPIC -shared function.c -o libfunc.so 3 python3 MemoryTest.py
II) 测试结果如下(测试平台: Ubuntu 18.04.6 LTS + Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz ):
Line 91 : TimeCost: 0.07772445678710938 ms 102KB Speed=1.25GB/s
二、采用共享内存方式进行IPC通信
内存共享基本方法参考 《进程间通信原理》 ,共享内存的方式需要通过其他通信方式进行进程间数据同步,从而保证共享内存在使用的过程中不被其他进程修改。
main.py
1 from ctypes import * 2 import numpy as np 3 import codecs 4 import datetime 5 6 SHM_SIZE = 1024*1024*20 # 20MBytes 7 SHM_KEY = 123559 8 9 OUTFILE="Shared.PNG" 10 try: 11 rt = CDLL('librt.so') 12 except: 13 rt = CDLL('librt.so.1') 14 15 shmget = rt.shmget 16 shmget.argtypes = [c_int, c_size_t, c_int] 17 shmget.restype = c_int 18 shmat = rt.shmat 19 shmat.argtypes = [c_int, POINTER(c_void_p), c_int] 20 shmat.restype = c_void_p 21 22 shmid = shmget(SHM_KEY, SHM_SIZE, 0o666) 23 24 if shmid < 0: 25 print ("System not infected") 26 else: 27 addr = shmat(shmid, None, 0) 28 f=open(OUTFILE, 'wb') 29 begin_time = datetime.datetime.now() 30 DataLength = int.from_bytes(string_at(addr,4), byteorder='little', signed=True) #这里数据文件是小端int16类型 31 ImgData = string_at(addr+4,DataLength) 32 end_time = datetime.datetime.now() 33 print(DataLength, ' Bytes') 34 print('Type:',type(ImgData),' Bytes:', len(ImgData)) 35 f.write(ImgData) 36 f.close() 37 #print ("Dumped %d bytes in %s" % (SHM_SIZE, OUTFILE)) 38 print("Success!",end_time-begin_time)
View Code
main.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/shm.h> 4 #include <string.h> 5 #include <time.h> 6 #include <sys/time.h> 7 8 #define SHAERD_MEM_SIZE 20 * 1024 * 1024 // 20MBytes 9 10 char mem_free(void *ptr); 11 time_t get_timestamp_ms(void); 12 char *file_read(unsigned int *file_bytes, char *file_name); 13 14 int main(int argc, char *argv[]) 15 { 16 int id = 0; 17 size_t offset = 0; 18 char *data = NULL; 19 char *ImgData = NULL; 20 unsigned int file_bytes = 0; 21 time_t start_time, end_time; 22 if (argc < 2) 23 { 24 printf("args too less/n"); 25 return 0; 26 } 27 28 id = shmget(123559, SHAERD_MEM_SIZE, IPC_CREAT | 0777); 29 if (id < 0) 30 { 31 printf("get id failed/n"); 32 return 0; 33 } 34 35 data = shmat(id, NULL, 0); 36 if (data == NULL) 37 { 38 printf("shamt failed/n"); 39 return 0; 40 } 41 42 ImgData = file_read(&file_bytes, argv[1]); 43 offset = sizeof(unsigned int); 44 printf("Size of unsigned long:%d/n", offset); 45 printf("Size of Image File:%d/n", file_bytes); 46 start_time = get_timestamp_ms(); 47 memcpy(data, &file_bytes, sizeof(unsigned int)); 48 memcpy(data + offset, ImgData, file_bytes); 49 end_time = get_timestamp_ms(); 50 51 printf("Time Cost:%d/n", end_time - start_time); 52 53 mem_free(ImgData); 54 55 return 0; 56 } 57 58 char *file_read(unsigned int *file_bytes, char *file_name) 59 { 60 int file_size; 61 FILE *fd = NULL; 62 char *file_data = NULL; 63 fd = fopen(file_name, "rw"); 64 if(fd < 0) 65 { 66 printf("File open failed.../n"); 67 return NULL; 68 } 69 fseek(fd, 0, SEEK_END); 70 file_size = ftell (fd); 71 file_data = malloc(sizeof(char)*file_size); 72 if(file_data == NULL) 73 { 74 printf("Malloc failed.../n"); 75 return NULL; 76 } 77 fseek(fd, 0, SEEK_SET); 78 *file_bytes = fread(file_data,sizeof(char),file_size,fd); 79 fclose(fd); 80 return file_data; 81 } 82 83 time_t get_timestamp_ms(void) 84 { 85 time_t timestamp_ms = 0; 86 struct timeval tv; 87 88 gettimeofday(&tv,NULL); 89 timestamp_ms = tv.tv_sec * 1000 + tv.tv_usec / 1000; 90 return timestamp_ms; 91 } 92 93 char mem_free(void *ptr) 94 { 95 if(NULL != ptr) 96 { 97 free(ptr); 98 return 0; 99 } 100 printf("Memory is Empty.../n"); 101 return -1; 102 }
View Code
编译执行即可
gcc -o main main.c ./main sdlinux.zip # 12MB python3 main.py
测试结果如下(测试平台:Ubuntu 18.04.6 LTS + Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz ):
Success! 7.091045379638672 ms 12MB Speed=1.67GB/s
未完待续 ~
原创文章,作者:端木书台,如若转载,请注明出处:https://blog.ytso.com/244399.html